From 906e5a16cd0dc5e50507794baa3b35c0bc994467 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 14 Jun 2015 01:01:50 -0700 Subject: when there are duplicate username@domain records and selfservice_server-primary_only is enabled, allow login with a primary service, RT#30750 --- FS/FS/ClientAPI/MyAccount.pm | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index cb6ac02d8..ec76e2738 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -260,16 +260,39 @@ sub login { my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } ) or return { error => 'Domain '. $p->{'domain'}. ' not found' }; - my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{'username'}, - 'domsvc' => $svc_domain->svcnum, } - ); - return { error => 'User not found.' } unless $svc_acct; + my @svc_acct = qsearch( 'svc_acct', { 'username' => $p->{'username'}, + 'domsvc' => $svc_domain->svcnum, } + ); + + if ( $conf->exists('selfservice_server-login_svcpart') ) { + my @svcpart = $conf->config('selfservice_server-login_svcpart'); + @svc_acct = grep { my $svcpart = $_->cust_svc->svcpart; + scalar( grep( $_ eq $svcpart, @svcpart ) ); + } + @svc_acct; + } + + if ( $conf->exists('selfservice_server-primary_only') ) { + @svc_acct = + grep { + my $cust_svc = $_->cust_svc; + $cust_svc->cust_pkg->part_pkg->svcpart([qw( svc_acct svc_phone )]) + == $cust_svc->svcpart + } + @svc_acct; + } + + return { error => 'User not found.' } unless @svc_acct; + + #return { error => 'Multiple users.' } if scalar(@svc_acct) > 1; + + my $svc_acct = $svc_acct[0]; - if($conf->exists('selfservice_server-login_svcpart')) { - my @svcpart = $conf->config('selfservice_server-login_svcpart'); - my $svcpart = $svc_acct->cust_svc->svcpart; - return { error => 'Invalid user.' } - unless grep($_ eq $svcpart, @svcpart); + if ( $conf->exists('selfservice_server-login_svcpart') ) { + my @svcpart = $conf->config('selfservice_server-login_svcpart'); + my $svcpart = $svc_acct->cust_svc->svcpart; + return { error => 'Invalid user.' } + unless grep($_ eq $svcpart, @svcpart); } return { error => 'Incorrect password.' } -- cgit v1.2.1 From 2472b5d17c4788e3b7b076def566bcd1170ec7ba Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sun, 14 Jun 2015 01:05:39 -0700 Subject: when there are duplciate username@domain records, and neither selfservice_server-primary_only or selfservice_server-login-svcpart designate one for login, throw and error rather than picking one arbitrarily --- FS/FS/ClientAPI/MyAccount.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index ec76e2738..36989db22 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -284,7 +284,7 @@ sub login { return { error => 'User not found.' } unless @svc_acct; - #return { error => 'Multiple users.' } if scalar(@svc_acct) > 1; + return { error => 'Multiple users.' } if scalar(@svc_acct) > 1; my $svc_acct = $svc_acct[0]; -- cgit v1.2.1 From 9a963c59933af6813405d311d6de1cd5a4eb8597 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 15 Jun 2015 12:36:02 -0400 Subject: Ticket #34769 different enswitch format --- FS/FS/cdr/enswitch_calling_name.pm | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 FS/FS/cdr/enswitch_calling_name.pm (limited to 'FS') diff --git a/FS/FS/cdr/enswitch_calling_name.pm b/FS/FS/cdr/enswitch_calling_name.pm new file mode 100644 index 000000000..c5564d3a6 --- /dev/null +++ b/FS/FS/cdr/enswitch_calling_name.pm @@ -0,0 +1,62 @@ +package FS::cdr::enswitch_calling_name; +use base qw( FS::cdr ); + +use strict; +use vars qw( %info $tmp_mon $tmp_mday $tmp_year ); +use FS::Record qw( qsearchs ); +use FS::cdr_type; + +%info = ( + 'name' => 'Enswitch with calling name', + 'weight' => 515, + 'header' => 2, + 'type' => 'csv', + 'import_fields' => [ + 'dcontext', #Status + 'startdate', #Start, already a unix timestamp + skip(2), #Start date, Start time + 'enddate', #End + skip(6), #End date, End time + #Calling customer, Calling type + 'src', #Calling number + skip(1), #Called type + + sub { my ($cdr, $dst) = @_; + $dst =~ s/\*//g; + $cdr->set('dst', $dst); + }, #Called number + + skip(14), #Destination customer, Destination type + #Destination number + #Destination group ID, Destination group name, + #Inbound calling type, + #Inbound calling number, + #Inbound called type, + #Inbound called number, + #Inbound destination type, Inbound destination number, + sub { my ($cdr, $data) = @_; + $data ||= 'none'; + + my $cdr_type = qsearchs('cdr_type', { 'cdrtypename' => $data } ); + $cdr->set('cdrtypenum', $cdr_type->cdrtypenum) if $cdr_type; + } , #Outbound calling type, + + skip(11), #Outbound calling number, + #Outbound called type, Outbound called number, + #Outbound destination type, Outbound destination number, + #Internal calling type, Internal calling number, + #Internal called type, Internal called number, + #Internal destination type, Internal destination number + 'duration', #Total seconds + skip(1), #Ring seconds + 'billsec', #Billable seconds + skip(2), #Cost + #Cost including taxes + 'accountcode', #Billing customer + skip(3), #Billing customer name, Billing type, Billing reference + ], +); + +sub skip { map {''} (1..$_[0]) } + +1; -- cgit v1.2.1 From e9bf7c82e204b29ddca9f7d3179a7e4c5473b767 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 15 Jun 2015 12:04:21 -0700 Subject: self-service API: allow changing CVV with edit_info, RT#36795 --- FS/FS/ClientAPI/MyAccount.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 36989db22..85a96c7c5 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -822,16 +822,16 @@ sub edit_info { if ( $new->payinfo eq $cust_main->paymask ) { $new->payinfo($cust_main->payinfo); + $new->paycvv( $p->{'paycvv'} || $cust_main->paycvv ); } else { $new->payinfo($p->{'payinfo'}); + return { 'error' => 'CVV2 is required' } + if ! $p->{'paycvv'} && $conf->exists('selfservice-onfile_require_cvv'); + $new->paycvv( $p->{'paycvv'} ) } $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' ); - if ( $conf->exists('selfservice-onfile_require_cvv') ){ - return { 'error' => 'CVV2 is required' } unless $p->{'paycvv'}; - } - } elsif ( $payby =~ /^(CHEK|DCHK)$/ ) { my $payinfo; -- cgit v1.2.1 From 8f82d54c3b3bd30c8f8451ea24bcdab70ff37327 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 15 Jun 2015 23:37:48 -0500 Subject: RT#30705 Change contract end date when changing packages --- FS/FS/cust_pkg.pm | 77 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 9 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 91a5677f2..5bd307b0f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1943,6 +1943,13 @@ can't be transferred (also see the I config option). If unprotect_svcs is true, this method will transfer as many services as it can and then unconditionally cancel the old package. +=item contract_end + +If specified, sets this value for the contract_end date on the new package +(without regard for keep_dates or the usual date-preservation behavior.) +Will throw an error if defined but false; the UI doesn't allow editing +this unless it already exists, making removal impossible to undo. + =back At least one of locationnum, cust_location, pkgpart, refnum, cust_main, or @@ -1956,6 +1963,36 @@ For example: =cut +#used by change and change_later +#didn't put with documented check methods because it depends on change-specific opts +#and it also possibly edits the value of opts +sub _check_change { + my $self = shift; + my $opt = shift; + if ( defined($opt->{'contract_end'}) ) { + my $current_contract_end = $self->get('contract_end'); + unless ($opt->{'contract_end'}) { + if ($current_contract_end) { + return "Cannot remove contract end date when changing packages"; + } else { + #shouldn't even pass this option if there's not a current value + #but can be handled gracefully if the option is empty + warn "Contract end date passed unexpectedly"; + delete $opt->{'contract_end'}; + return ''; + } + } + unless ($current_contract_end) { + #option shouldn't be passed, throw error if it's non-empty + return "Cannot add contract end date when changing packages " . $self->pkgnum; + } + if ($opt->{'start_date'} && ($opt->{'contract_end'} < $opt->{'start_date'})) { + return "Contract end date is before change date"; + } + } + return ''; +} + #some false laziness w/order sub change { my $self = shift; @@ -1963,13 +2000,21 @@ sub change { my $conf = new FS::Conf; + # handle contract_end on cust_pkg same as passed option + if ( $opt->{'cust_pkg'} ) { + $opt->{'contract_end'} = $opt->{'cust_pkg'}->contract_end; + delete $opt->{'contract_end'} unless $opt->{'contract_end'}; + } + + # check contract_end, prevent adding/removing + my $error = $self->_check_change($opt); + return $error if $error; + # Transactionize this whole mess my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error; - if ( $opt->{'cust_location'} ) { $error = $opt->{'cust_location'}->find_or_insert; if ( $error ) { @@ -1994,6 +2039,9 @@ sub change { if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { $self->set_initial_timers; } + # but if contract_end was explicitly specified, that overrides all else + $self->set('contract_end', $opt->{'contract_end'}) + if $opt->{'contract_end'}; $error = $self->replace; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -2051,6 +2099,9 @@ sub change { start_date contract_end)) { $hash{$date} = $self->getfield($date); } + # but if contract_end was explicitly specified, that overrides all else + $hash{'contract_end'} = $opt->{'contract_end'} + if $opt->{'contract_end'}; # allow $opt->{'locationnum'} = '' to specifically set it to null # (i.e. customer default location) @@ -2339,8 +2390,10 @@ The date for the package change. Required, and must be in the future. =item quantity -The pkgpart. locationnum, and quantity of the new package, with the same -meaning as in C. +=item contract_end + +The pkgpart, locationnum, quantity and optional contract_end of the new +package, with the same meaning as in C. =back @@ -2350,6 +2403,10 @@ sub change_later { my $self = shift; my $opt = ref($_[0]) ? shift : { @_ }; + # check contract_end, prevent adding/removing + my $error = $self->_check_change($opt); + return $error if $error; + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; @@ -2363,8 +2420,6 @@ sub change_later { return "start_date $date is in the past"; } - my $error; - if ( $self->change_to_pkgnum ) { my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum); my $new_pkgpart = $opt->{'pkgpart'} @@ -2373,7 +2428,9 @@ sub change_later { if $opt->{'locationnum'} and $opt->{'locationnum'} != $change_to->locationnum; my $new_quantity = $opt->{'quantity'} if $opt->{'quantity'} and $opt->{'quantity'} != $change_to->quantity; - if ( $new_pkgpart or $new_locationnum or $new_quantity ) { + my $new_contract_end = $opt->{'contract_end'} + if $opt->{'contract_end'} and $opt->{'contract_end'} != $change_to->contract_end; + if ( $new_pkgpart or $new_locationnum or $new_quantity or $new_contract_end ) { # it hasn't been billed yet, so in principle we could just edit # it in place (w/o a package change), but that's bad form. # So change the package according to the new options... @@ -2413,8 +2470,10 @@ sub change_later { if $opt->{'locationnum'} and $opt->{'locationnum'} != $self->locationnum; my $new_quantity = $opt->{'quantity'} if $opt->{'quantity'} and $opt->{'quantity'} != $self->quantity; + my $new_contract_end = $opt->{'contract_end'} + if $opt->{'contract_end'} and $opt->{'contract_end'} != $self->contract_end; - return '' unless $new_pkgpart or $new_locationnum or $new_quantity; # wouldn't do anything + return '' unless $new_pkgpart or $new_locationnum or $new_quantity or $new_contract_end; # wouldn't do anything # allow $opt->{'locationnum'} = '' to specifically set it to null # (i.e. customer default location) @@ -2425,7 +2484,7 @@ sub change_later { locationnum => $opt->{'locationnum'}, start_date => $date, map { $_ => ( $opt->{$_} || $self->$_() ) } - qw( pkgpart quantity refnum salesnum ) + qw( pkgpart quantity refnum salesnum contract_end ) } ); $error = $new->insert('change' => 1, 'allow_pkgpart' => ($new_pkgpart ? 0 : 1)); -- cgit v1.2.1 From 395b48e2a32e90ae0edf2e45a31758dc78181f04 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 16 Jun 2015 20:47:57 -0700 Subject: disable virtual field access by default until it is working, RT#35178 --- FS/FS/AccessRight.pm | 2 ++ 1 file changed, 2 insertions(+) (limited to 'FS') diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 95e7aeab8..1609f085f 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -405,6 +405,8 @@ tie my %rights, 'Tie::IxHash', #{ rightname=>'Edit employees', global=>1, }, #{ rightname=>'Edit employee groupss', global=>1, }, + { rightname=>'Edit custom fields', global=>1 }, + { rightname=>'Configuration', global=>1 }, #most of the rest of the configuraiton is not agent-virtualized { rightname=>'Configuration download', }, #description of how it affects -- cgit v1.2.1 From d54ade7b3c18794d5ab79f707fc35f0da061306a Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 16 Jun 2015 23:31:15 -0700 Subject: web access logging to DB --- FS/FS/Schema.pm | 12 +++++ FS/FS/access_user_log.pm | 136 +++++++++++++++++++++++++++++++++++++++++++++++ FS/MANIFEST | 2 + FS/t/access_user_log.t | 5 ++ 4 files changed, 155 insertions(+) create mode 100644 FS/FS/access_user_log.pm create mode 100644 FS/t/access_user_log.t (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 2d98963a8..70d7c5afb 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5734,6 +5734,18 @@ sub tables_hashref { 'index' => [], }, + 'access_user_log' => { + 'columns' => [ + 'lognum', 'serial', '', '', '', '', + 'usernum', 'int', '', '', '', '', + 'path', 'varchar', '', 2*$char_d, '', '', + '_date', @date_type, '', '', + ], + 'primary_key' => 'lognum', + 'unique' => [], + 'index' => [ ['usernum'], ['path'], ['_date'] ], + }, + 'sched_item' => { 'columns' => [ 'itemnum', 'serial', '', '', '', '', diff --git a/FS/FS/access_user_log.pm b/FS/FS/access_user_log.pm new file mode 100644 index 000000000..884d250d6 --- /dev/null +++ b/FS/FS/access_user_log.pm @@ -0,0 +1,136 @@ +package FS::access_user_log; +use base qw( FS::Record ); + +use strict; +#use FS::Record qw( qsearch qsearchs ); +use FS::CurrentUser; + +=head1 NAME + +FS::access_user_log - Object methods for access_user_log records + +=head1 SYNOPSIS + + use FS::access_user_log; + + $record = new FS::access_user_log \%hash; + $record = new FS::access_user_log { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::access_user_log object represents a backoffice web server log entry. + FS::access_user_log inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item lognum + +primary key + +=item usernum + +usernum + +=item path + +path + +=item _date + +_date + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new log entry. To add the log entry to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'access_user_log'; } + +=item insert_new_path PATH + +Adds a log entry for PATH for the current user and timestamp. + +=cut + +sub insert_new_path { + my( $class, $path ) = @_; + + my $self = $class->new( { + 'usernum' => $FS::CurrentUser::CurrentUser->usernum, + 'path' => $path, + '_date' => time, + } ); + + my $error = $self->insert; + die $error if $error; + +} + +=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 log entry. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('lognum') + || $self->ut_foreign_key('usernum', 'access_user', 'usernum') + || $self->ut_text('path') + || $self->ut_number('_date') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 422f69c0e..899270bf2 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -846,3 +846,5 @@ FS/cust_pkg_reason_fee.pm t/cust_pkg_reason_fee.t FS/part_svc_link.pm t/part_svc_link.t +FS/access_user_log.pm +t/access_user_log.t diff --git a/FS/t/access_user_log.t b/FS/t/access_user_log.t new file mode 100644 index 000000000..582b32ca1 --- /dev/null +++ b/FS/t/access_user_log.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::access_user_log; +$loaded=1; +print "ok 1\n"; -- cgit v1.2.1 From aaeaf74ce3d3ceb731633a054d56a7f916ff2721 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 17 Jun 2015 14:39:29 -0700 Subject: fix new page logging --- FS/FS/Mason.pm | 2 ++ FS/FS/Mason/Request.pm | 3 +++ 2 files changed, 5 insertions(+) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 78779d78a..6163197fa 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -402,6 +402,7 @@ if ( -e $addl_handler_use_file ) { use FS::quotation_pkg_tax; use FS::cust_pkg_reason_fee; use FS::part_svc_link; + use FS::access_user_log; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { @@ -460,6 +461,7 @@ if ( -e $addl_handler_use_file ) { die $@ if $@; } + no warnings 'redefine'; *CGI::redirect = sub { my $self = shift; my $cookie = ''; diff --git a/FS/FS/Mason/Request.pm b/FS/FS/Mason/Request.pm index 5d6fc4cd4..62bf670b9 100644 --- a/FS/FS/Mason/Request.pm +++ b/FS/FS/Mason/Request.pm @@ -5,6 +5,7 @@ use warnings; use vars qw( $FSURL $QUERY_STRING ); use base 'HTML::Mason::Request'; use FS::Trace; +use FS::access_user_log; $FSURL = 'http://Set/FS_Mason_Request_FSURL/in_standalone_mode/'; $QUERY_STRING = ''; @@ -119,6 +120,8 @@ sub freeside_setup { } + FS::access_user_log->insert_new_path( $filename ); + FS::Trace->log(' done'); } -- cgit v1.2.1 From 09f899143460b0e99388ef007ff262f9a5e80203 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 17 Jun 2015 19:06:38 -0700 Subject: make "credit lineitems" feature work with new tax workflow, #18676, #25718, #31639 --- FS/FS/cust_credit.pm | 291 +++++++++++++++++++++++++++------------------------ 1 file changed, 154 insertions(+), 137 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index f63d86f99..01ee89dc0 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -705,6 +705,102 @@ sub credited_sql { unapplied_sql(); } +=item calculate_tax_adjustment PARAMS + +Calculate the amount of tax that needs to be credited as part of a lineitem +credit. + +PARAMS must include: + +- billpkgnums: arrayref identifying the line items to credit +- setuprecurs: arrayref of 'setup' or 'recur', indicating which part of + the lineitem charge is being credited +- amounts: arrayref of the amounts to credit on each line item +- custnum: the customer all of these invoices belong to, for error checking + +Returns a hash containing: +- subtotal: the total non-tax amount to be credited (the sum of the 'amounts') +- taxtotal: the total tax amount to be credited +- taxlines: an arrayref of hashrefs for each tax line to be credited, each with: + - table: "cust_bill_pkg_tax_location" or "cust_bill_pkg_tax_rate_location" + - num: the key within that table + - credit: the credit amount to apply to that line + +=cut + +sub calculate_tax_adjustment { + my ($class, %arg) = @_; + + my $error; + my @taxlines; + my $subtotal = 0; + my $taxtotal = 0; + + my (%cust_bill_pkg, %cust_bill); + + for (my $i = 0; ; $i++) { + my $billpkgnum = $arg{billpkgnums}[$i] + or last; + my $setuprecur = $arg{setuprecurs}[$i]; + my $amount = $arg{amounts}[$i]; + next if $amount == 0; + $subtotal += $amount; + my $cust_bill_pkg = $cust_bill_pkg{$billpkgnum} + ||= FS::cust_bill_pkg->by_key($billpkgnum) + or die "lineitem #$billpkgnum not found\n"; + + my $invnum = $cust_bill_pkg->invnum; + $cust_bill{ $invnum } ||= FS::cust_bill->by_key($invnum); + $cust_bill{ $invnum}->custnum == $arg{custnum} + or die "lineitem #$billpkgnum not found\n"; + + # calculate credit ratio. + # (First deduct any existing credits applied to this line item, to avoid + # rounding errors.) + my $charged = $cust_bill_pkg->get($setuprecur); + my $previously_credited = + $cust_bill_pkg->credited( '', '', setuprecur => $setuprecur) || 0; + + $charged -= $previously_credited; + if ($charged < $amount) { + $error = "invoice #$invnum: tried to credit $amount, but only $charged was charged"; + last; + } + my $ratio = $amount / $charged; + + # gather taxes that apply to the selected item + foreach my $table ( + qw(cust_bill_pkg_tax_location cust_bill_pkg_tax_rate_location) + ) { + foreach my $tax_link ( + qsearch($table, { taxable_billpkgnum => $billpkgnum }) + ) { + my $tax_amount = $tax_link->amount; + # deduct existing credits applied to the tax, for the same reason as + # above + foreach ($tax_link->cust_credit_bill_pkg) { + $tax_amount -= $_->amount; + } + my $tax_credit = sprintf('%.2f', $tax_amount * $ratio); + my $pkey = $tax_link->get($tax_link->primary_key); + push @taxlines, { + table => $table, + num => $pkey, + credit => $tax_credit, + }; + $taxtotal += $tax_credit; + + } #foreach cust_bill_pkg_tax_(rate_)?location + } + } # foreach $billpkgnum + + return ( + subtotal => sprintf('%.2f', $subtotal), + taxtotal => sprintf('%.2f', $taxtotal), + taxlines => \@taxlines, + ); +} + =item credit_lineitems Example: @@ -726,6 +822,8 @@ Example: =cut +use Data::Dumper; #XXX + #maybe i should just be an insert with extra args instead of a class method sub credit_lineitems { my( $class, %arg ) = @_; @@ -784,8 +882,12 @@ sub credit_lineitems { my %cust_credit_bill = (); my %cust_bill_pkg = (); my %cust_credit_bill_pkg = (); - my %taxlisthash = (); my %unapplied_payments = (); #invoice numbers, and then billpaynums + + # determine the tax adjustments + my %tax_adjust = $class->calculate_tax_adjustment(%arg); + + warn Dumper \%arg; foreach my $billpkgnum ( @{$arg{billpkgnums}} ) { my $setuprecur = shift @{$arg{setuprecurs}}; my $amount = shift @{$arg{amounts}}; @@ -799,22 +901,21 @@ sub credit_lineitems { my $invnum = $cust_bill_pkg->invnum; - if ( $setuprecur eq 'setup' ) { - $cust_bill_pkg->setup($amount); - $cust_bill_pkg->recur(0); - $cust_bill_pkg->unitrecur(0); - $cust_bill_pkg->type(''); - } else { - $setuprecur = 'recur'; #in case its a usage classnum? - $cust_bill_pkg->recur($amount); - $cust_bill_pkg->setup(0); - $cust_bill_pkg->unitsetup(0); - } - push @{$cust_bill_pkg{$invnum}}, $cust_bill_pkg; - #unapply any payments applied to this line item (other credits too?) - foreach my $cust_bill_pay_pkg ( $cust_bill_pkg->cust_bill_pay_pkg($setuprecur) ) { + $cust_credit_bill{$invnum} += $amount; + push @{ $cust_credit_bill_pkg{$invnum} }, + new FS::cust_credit_bill_pkg { + 'billpkgnum' => $billpkgnum, + 'amount' => sprintf('%.2f',$amount), + 'setuprecur' => $setuprecur, + 'sdate' => $cust_bill_pkg->sdate, + 'edate' => $cust_bill_pkg->edate, + }; + # unapply payments (but not other credits) from this line item + foreach my $cust_bill_pay_pkg ( + $cust_bill_pkg->cust_bill_pay_pkg($setuprecur) + ) { $error = $cust_bill_pay_pkg->delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -823,24 +924,49 @@ sub credit_lineitems { $unapplied_payments{$invnum}{$cust_bill_pay_pkg->billpaynum} += $cust_bill_pay_pkg->amount; } + } + + # do the same for taxes + foreach my $tax_credit ( @{ $tax_adjust{taxlines} } ) { + my $table = $tax_credit->{table}; + my $tax_link = "FS::$table"->by_key( $tax_credit->{num} ) + or die "tried to credit $table #$tax_credit->{num} but it doesn't exist"; + + my $billpkgnum = $tax_link->billpkgnum; + my $cust_bill_pkg = qsearchs({ + 'table' => 'cust_bill_pkg', + 'hashref' => { 'billpkgnum' => $billpkgnum }, + 'addl_from' => 'LEFT JOIN cust_bill USING (invnum)', + 'extra_sql' => 'AND custnum = '. $cust_main->custnum, + }) or die "unknown billpkgnum $billpkgnum"; + + my $invnum = $cust_bill_pkg->invnum; + push @{$cust_bill_pkg{$invnum}}, $cust_bill_pkg; - #$subtotal += $amount; + my $amount = $tax_credit->{credit}; $cust_credit_bill{$invnum} += $amount; + + # create a credit application record to the tax line item, earmarked + # to the specific cust_bill_pkg_Xlocation push @{ $cust_credit_bill_pkg{$invnum} }, new FS::cust_credit_bill_pkg { - 'billpkgnum' => $cust_bill_pkg->billpkgnum, - 'amount' => sprintf('%.2f',$amount), - 'setuprecur' => $setuprecur, - 'sdate' => $cust_bill_pkg->sdate, - 'edate' => $cust_bill_pkg->edate, + 'billpkgnum' => $billpkgnum, + 'amount' => sprintf('%.2f', $amount), + 'setuprecur' => 'setup', + $tax_link->primary_key, $tax_credit->{num} }; - - # recalculate taxes with new amounts - $taxlisthash{$invnum} ||= {}; - if ( $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart ) { - $cust_main->_handle_taxes( $taxlisthash{$invnum}, $cust_bill_pkg ); - } # otherwise the item itself is a tax, and assume the caller knows - # what they're doing + # unapply any payments from the tax + foreach my $cust_bill_pay_pkg ( + $cust_bill_pkg->cust_bill_pay_pkg('setup') + ) { + $error = $cust_bill_pay_pkg->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error unapplying payment: $error"; + } + $unapplied_payments{$invnum}{$cust_bill_pay_pkg->billpaynum} + += $cust_bill_pay_pkg->amount; + } } ### @@ -852,115 +978,6 @@ sub credit_lineitems { foreach my $invnum ( sort { $a <=> $b } keys %cust_credit_bill ) { - local $@; - my $arrayref_or_error = eval { $cust_main->calculate_taxes( - $cust_bill_pkg{$invnum}, # list of taxable items that we're crediting - $taxlisthash{$invnum}, # list of tax-item bindings - $cust_bill_pkg{$invnum}->[0]->cust_bill->_date, # invoice time - ) }; - - if ( $@ ) { - $dbh->rollback if $oldAutoCommit; - return "Error calculating taxes: $@"; - } - - my %tax_links; # {tax billpkgnum}{nontax billpkgnum} - - #taxes - foreach my $cust_bill_pkg ( @{ $cust_bill_pkg{$invnum} } ) { - my $billpkgnum = $cust_bill_pkg->billpkgnum; - my %hash = ( 'taxable_billpkgnum' => $billpkgnum ); - # gather up existing tax links (we need their billpkgtaxlocationnums) - my @tax_links = qsearch('cust_bill_pkg_tax_location', \%hash), - qsearch('cust_bill_pkg_tax_rate_location', \%hash); - - foreach ( @tax_links ) { - $tax_links{$_->billpkgnum} ||= {}; - $tax_links{$_->billpkgnum}{$_->taxable_billpkgnum} = $_; - } - } - - foreach my $taxline ( @$arrayref_or_error ) { - - my $amount = $taxline->setup; - - # find equivalent tax line item on the existing invoice - my $tax_item = qsearchs('cust_bill_pkg', { - 'invnum' => $invnum, - 'pkgnum' => 0, - 'itemdesc' => $taxline->desc, - }); - if (!$tax_item) { - # or should we just exit if this happens? - $cust_credit->set('amount', - sprintf('%.2f', $cust_credit->get('amount') - $amount) - ); - my $error = $cust_credit->replace; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "error correcting credit for missing tax line: $error"; - } - } - - # but in the new era, we no longer have the problem of uniquely - # identifying the tax_Xlocation record. The billpkgnums of the - # tax and the taxed item are known. - foreach my $new_loc - ( @{ $taxline->get('cust_bill_pkg_tax_location') }, - @{ $taxline->get('cust_bill_pkg_tax_rate_location') } ) - { - # the existing tax_Xlocation object - my $old_loc = - $tax_links{$tax_item->billpkgnum}{$new_loc->taxable_cust_bill_pkg->billpkgnum}; - - next if !$old_loc; # apply the leftover amount nonspecifically - - #support partial credits: use $amount if smaller - # (so just distribute to the first location? perhaps should - # do so evenly...) - my $loc_amount = min( $amount, $new_loc->amount); - - $amount -= $loc_amount; - - $cust_credit_bill{$invnum} += $loc_amount; - push @{ $cust_credit_bill_pkg{$invnum} }, - new FS::cust_credit_bill_pkg { - 'billpkgnum' => $tax_item->billpkgnum, - 'amount' => $loc_amount, - 'setuprecur' => 'setup', - 'billpkgtaxlocationnum' => $old_loc->billpkgtaxlocationnum, - 'billpkgtaxratelocationnum' => $old_loc->billpkgtaxratelocationnum, - }; - - } #foreach my $new_loc - - # we still have to deal with the possibility that the tax links don't - # cover the whole amount of tax because of an incomplete upgrade... - if ($amount > 0.005) { - $cust_credit_bill{$invnum} += $amount; - push @{ $cust_credit_bill_pkg{$invnum} }, - new FS::cust_credit_bill_pkg { - 'billpkgnum' => $tax_item->billpkgnum, - 'amount' => sprintf('%.2f', $amount), - 'setuprecur' => 'setup', - }; - - } # if $amount > 0 - - #unapply any payments applied to the tax - foreach my $cust_bill_pay_pkg - ( $tax_item->cust_bill_pay_pkg('setup') ) - { - $error = $cust_bill_pay_pkg->delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "Error unapplying payment: $error"; - } - $unapplied_payments{$invnum}{$cust_bill_pay_pkg->billpaynum} - += $cust_bill_pay_pkg->amount; - } - } #foreach $taxline - # if we unapplied any payments from line items, also unapply that # amount from the invoice foreach my $billpaynum (keys %{$unapplied_payments{$invnum}}) { -- cgit v1.2.1 From e653b204c7da74200345c1718ffda973ec8c6cee Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 18 Jun 2015 14:50:57 -0700 Subject: debug --- FS/FS/cust_credit.pm | 3 --- 1 file changed, 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 01ee89dc0..544a0e83d 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -822,8 +822,6 @@ Example: =cut -use Data::Dumper; #XXX - #maybe i should just be an insert with extra args instead of a class method sub credit_lineitems { my( $class, %arg ) = @_; @@ -887,7 +885,6 @@ sub credit_lineitems { # determine the tax adjustments my %tax_adjust = $class->calculate_tax_adjustment(%arg); - warn Dumper \%arg; foreach my $billpkgnum ( @{$arg{billpkgnums}} ) { my $setuprecur = shift @{$arg{setuprecurs}}; my $amount = shift @{$arg{amounts}}; -- cgit v1.2.1 From 9ccb619364f1a04a98d914cd79bc7a75a9e196cb Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 18 Jun 2015 17:15:50 -0700 Subject: silence many spurious warnings from part_pkg options --- FS/FS/part_pkg.pm | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'FS') diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 4407ec6dd..0e9ee05fb 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -1392,6 +1392,11 @@ sub option { my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); } split("\n", $self->get('plandata') ); return $plandata{$opt} if exists $plandata{$opt}; + + # check whether the option is defined in plan info (if so, don't warn) + if (exists $plans{ $self->plan }->{fields}->{$opt}) { + return ''; + } cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ". "not found in options or plandata!\n" unless $ornull; -- cgit v1.2.1 From ed4675557ac1b7012bc1c1607a73070f052bede5 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 18 Jun 2015 17:15:54 -0700 Subject: apply global default rates to calls outside every region, #35199, from #30633 --- FS/FS/rate.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/rate.pm b/FS/FS/rate.pm index a3826bff2..8ee9a83be 100644 --- a/FS/FS/rate.pm +++ b/FS/FS/rate.pm @@ -347,7 +347,7 @@ sub dest_detail { }); } - return '' unless $rate_prefix; + return $self->default_detail unless $rate_prefix; $regionnum = $rate_prefix->regionnum; -- cgit v1.2.1 From 11ca9a51a76837f1821b2b0e8972c78bf221c6a1 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 22 Jun 2015 13:14:33 -0700 Subject: invoice watermarks, #24665 --- FS/FS/Conf.pm | 20 ++++++++++++++++++++ FS/FS/Schema.pm | 2 ++ FS/FS/Template_Mixin.pm | 39 +++++++++++++++++++++------------------ FS/FS/invoice_conf.pm | 6 ++++++ 4 files changed, 49 insertions(+), 18 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 9255284a3..191a712a8 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -605,10 +605,12 @@ invoice_latexfooter invoice_latexsmallfooter invoice_latexnotes invoice_latexcoupon +invoice_latexwatermark invoice_html invoice_htmlreturnaddress invoice_htmlfooter invoice_htmlnotes +invoice_htmlwatermark logo.png logo.eps ); @@ -1303,6 +1305,15 @@ sub reason_type_options { 'per_locale' => 1, }, + { + 'key' => 'invoicehtmlwatermark', + 'section' => 'invoicing', + 'description' => 'Watermark for HTML invoices. Appears in a semitransparent positioned DIV overlaid on the main invoice container.', + 'type' => 'textarea', + 'per_agent' => 1, + 'per_locale' => 1, + }, + { 'key' => 'invoice_latex', 'section' => 'invoicing', @@ -1490,6 +1501,15 @@ and customer address. Include units.', 'per_locale' => 1, }, + { + 'key' => 'invoicelatexwatermark', + 'section' => 'invoicing', + 'description' => 'Watermark for LaTeX invoices. See "texdoc background" for information on what this can contain. The content itself should be enclosed in braces, optionally followed by a comma and any formatting options.', + 'type' => 'textarea', + 'per_agent' => 1, + 'per_locale' => 1, + }, + { 'key' => 'invoice_email_pdf', 'section' => 'invoicing', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 70d7c5afb..0bdc99539 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -6914,6 +6914,8 @@ sub tables_hashref { 'latexsmallfooter', 'text', 'NULL', '', '', '', 'latexreturnaddress', 'text', 'NULL', '', '', '', 'with_latexcoupon', 'char', 'NULL', '1', '', '', + 'htmlwatermark', 'text', 'NULL', '', '', '', + 'latexwatermark', 'text', 'NULL', '', '', '', 'lpr', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'confnum', diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm index 32e300776..37dcf2a5e 100644 --- a/FS/FS/Template_Mixin.pm +++ b/FS/FS/Template_Mixin.pm @@ -817,35 +817,36 @@ sub print_generic { my @include = ( [ $tc, 'notes' ], [ 'invoice_', 'footer' ], [ 'invoice_', 'smallfooter', ], + [ 'invoice_', 'watermark' ], ); push @include, [ $tc, 'coupon', ] unless $params{'no_coupon'}; foreach my $i (@include) { + # load the configuration for this sub-template + my($base, $include) = @$i; my $inc_file = $conf->key_orbase("$base$format$include", $template); - my @inc_src; - - if ( $conf->exists($inc_file, $agentnum) - && length( $conf->config($inc_file, $agentnum) ) ) { - - @inc_src = $conf->config($inc_file, $agentnum); - - } else { - - $inc_file = $conf->key_orbase("${base}latex$include", $template); - - my $convert_map = $convert_maps{$format}{$include}; - @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g; - s/--\@\]/$delimiters{$format}[1]/g; - $_; - } - &$convert_map( $conf->config($inc_file, $agentnum) ); + my @inc_src = $conf->config($inc_file, $agentnum); + if (!@inc_src) { + my $converter = $convert_maps{$format}{$include}; + if ( $converter ) { + # then attempt to convert LaTeX to the requested format + $inc_file = $conf->key_orbase($base.'latex'.$include, $template); + @inc_src = &$converter( $conf->config($inc_file, $agentnum) ); + foreach (@inc_src) { + # this isn't included in the convert_maps + my ($open, $close) = @{ $delimiters{$format} }; + s/\[\@--/$open/g; + s/--\@\]/$close/g; + } + } + } # else @inc_src is empty and that's fine - } + # make a Text::Template out of it my $inc_tt = new Text::Template ( TYPE => 'ARRAY', @@ -859,6 +860,8 @@ sub print_generic { die $error; } + # fill in variables + $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data ); $invoice_data{$include} =~ s/\n+$// diff --git a/FS/FS/invoice_conf.pm b/FS/FS/invoice_conf.pm index da448b816..d88c89a7c 100644 --- a/FS/FS/invoice_conf.pm +++ b/FS/FS/invoice_conf.pm @@ -49,6 +49,8 @@ and supports the FS::Conf interface. The following fields are supported: =item htmlreturnaddress - return address (HTML) +=item htmlwatermark - watermark to show in background (HTML) + =item latexnotes - "notes" section (LaTeX) =item latexfooter - footer (LaTeX) @@ -59,6 +61,8 @@ and supports the FS::Conf interface. The following fields are supported: =item latexsmallfooter - footer for pages after the first (LaTeX) +=item latexwatermark - watermark to show in background (LaTeX) + =item with_latexcoupon - 'Y' to print the payment coupon (LaTeX) =item lpr - command to print the invoice (passed on stdin as a PDF) @@ -185,11 +189,13 @@ sub check { || $self->ut_anything('htmlfooter') || $self->ut_anything('htmlsummary') || $self->ut_anything('htmlreturnaddress') + || $self->ut_anything('htmlwatermark') || $self->ut_anything('latexnotes') || $self->ut_anything('latexfooter') || $self->ut_anything('latexsummary') || $self->ut_anything('latexsmallfooter') || $self->ut_anything('latexreturnaddress') + || $self->ut_anything('latexwatermark') # flags || $self->ut_flag('with_latexcoupon') ; -- cgit v1.2.1 From c6ef5a3a043c4fafa2f8d21028609f1b9b70eb47 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 22 Jun 2015 18:34:27 -0500 Subject: RT#34078: Payment History Report / Statement --- FS/FS/ClientAPI/MyAccount.pm | 70 ++------------ FS/FS/Conf.pm | 7 ++ FS/FS/cust_main.pm | 198 ++++++++++++++++++++++++++++++++++++++ FS/FS/cust_main_Mixin.pm | 11 ++- FS/FS/msg_template.pm | 37 ++++++- FS/FS/msg_template/InitialData.pm | 9 ++ 6 files changed, 264 insertions(+), 68 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 85a96c7c5..610754c7a 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -696,73 +696,15 @@ sub billing_history { $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} ) : '(none)'; - 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( $cust_main->locale ). - ( $_->sdate && $_->edate - ? ' '. time2str('%d-%b-%Y', $_->sdate). - ' To '. time2str('%d-%b-%Y', $_->edate) - : '' - ), - 'amount' => sprintf('%.2f', $_->setup + $_->recur ), - 'date' => $cust_bill->_date, - 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), - } - foreach $cust_bill->cust_bill_pkg; - - } - - } else { + $return{'history'} = [ + $cust_main->payment_history( + 'line_items' => $conf->exists('selfservice-billing_history-line_items'), + 'reverse_sort' => 1, + ) + ]; - 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{'money_char'} = $conf->config("money_char") || '$', return \%return; diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 191a712a8..f3e244777 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2654,6 +2654,13 @@ and customer address. Include units.', 'select_enum' => [ 'text/plain', 'text/html' ], }, + { + 'key' => 'payment_history_msgnum', + 'section' => 'notification', + 'description' => 'Template to use for sending payment history to customer', + %msg_template_options, + }, + { 'key' => 'payby', 'section' => 'billing', diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f102d97ee..b7efa180f 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4095,6 +4095,204 @@ my ($self,$field) = @_; } +=item payment_history + +Returns an array of hashrefs standardizing information from cust_bill, cust_pay, +cust_credit and cust_refund objects. Each hashref has the following fields: + +I - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous' + +I - value of _date field, unix timestamp + +I - user-friendly date + +I - user-friendly description of item + +I - impact of item on user's balance +(positive for Invoice/Refund/Line item, negative for Payment/Credit.) +Not to be confused with the native 'amount' field in cust_credit, see below. + +I - includes money char + +I - customer balance, chronologically as of this item + +I - includes money char + +I - amount charged for cust_bill (Invoice or Line item) records, undef for other types + +I - amount paid for cust_pay records, undef for other types + +I - amount credited for cust_credit records, undef for other types. +Literally the 'amount' field from cust_credit, renamed here to avoid confusion. + +I - amount refunded for cust_refund records, undef for other types + +The four table-specific keys always have positive values, whether they reflect charges or payments. + +The following options may be passed to this method: + +I - if true, returns charges ('Line item') rather than invoices + +I - unix timestamp, only include records on or after. +If specified, an item of type 'Previous' will also be included. +It does not have table-specific fields. + +I - unix timestamp, only include records before + +I - order from newest to oldest (default is oldest to newest) + +I - optional already-loaded FS::Conf object. + +=cut + +# Caution: this gets used by FS::ClientAPI::MyAccount::billing_history, +# and also payment_history_text, which should both be kept customer-friendly. +# If you add anything that shouldn't be passed on through the API or exposed +# to customers, add a new option to include it, don't include it by default +sub payment_history { + my $self = shift; + my $opt = ref($_[0]) ? $_[0] : { @_ }; + + my $conf = $$opt{'conf'} || new FS::Conf; + my $money_char = $conf->config("money_char") || '$', + + #first load entire history, + #need previous to calculate previous balance + #loading after end_date shouldn't hurt too much? + my @history = (); + if ( $$opt{'line_items'} ) { + + foreach my $cust_bill ( $self->cust_bill ) { + + push @history, { + 'type' => 'Line item', + 'description' => $_->desc( $self->locale ). + ( $_->sdate && $_->edate + ? ' '. time2str('%d-%b-%Y', $_->sdate). + ' To '. time2str('%d-%b-%Y', $_->edate) + : '' + ), + 'amount' => sprintf('%.2f', $_->setup + $_->recur ), + 'charged' => 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 ), + 'charged' => sprintf('%.2f', $_->charged ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_bill; + + } + + push @history, { + 'type' => 'Payment', + 'description' => 'Payment', #XXX type + 'amount' => sprintf('%.2f', 0 - $_->paid ), + 'paid' => sprintf('%.2f', $_->paid ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_pay; + + push @history, { + 'type' => 'Credit', + 'description' => 'Credit', #more info? + 'amount' => sprintf('%.2f', 0 -$_->amount ), + 'credit' => sprintf('%.2f', $_->amount ), + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_credit; + + push @history, { + 'type' => 'Refund', + 'description' => 'Refund', #more info? type, like payment? + 'amount' => $_->refund, + 'refund' => $_->refund, + 'date' => $_->_date, + 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + } + foreach $self->cust_refund; + + #put it all in chronological order + @history = sort { $a->{'date'} <=> $b->{'date'} } @history; + + #calculate balance, filter items outside date range + my $previous = 0; + my $balance = 0; + my @out = (); + foreach my $item (@history) { + last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'}); + $balance += $$item{'amount'}; + if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) { + $previous += $$item{'amount'}; + next; + } + $$item{'balance'} = sprintf("%.2f",$balance); + foreach my $key ( qw(amount balance) ) { + $$item{$key.'_pretty'} = $$item{$key}; + $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/; + } + push(@out,$item); + } + + # start with previous balance, if there was one + if ($previous) { + my $item = { + 'type' => 'Previous', + 'description' => 'Previous balance', + 'amount' => sprintf("%.2f",$previous), + 'balance' => sprintf("%.2f",$previous), + }; + #false laziness with above + foreach my $key ( qw(amount balance) ) { + $$item{$key.'_pretty'} = $$item{$key}; + $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/; + } + unshift(@out,$item); + } + + @out = reverse @history if $$opt{'reverse_sort'}; + + return @out; +} + +=item payment_history_text + +Accepts the same options as L and returns those +results as a string table with fixed-width columns, max width 80 char. + +=cut + +sub payment_history_text { + my $self = shift; + my $opt = ref($_[0]) ? $_[0] : { @_ }; + my $out = sprintf("%-12s",'Date'); + $out .= sprintf("%11s",'Amount') . ' '; + $out .= sprintf("%11s",'Balance') . ' '; + $out .= 'Description'; #don't need to pad with spaces + $out .= "\n"; + foreach my $item ($self->payment_history($opt)) { + $out .= sprintf("%-10.10s",$$item{'date_pretty'}) . ' '; #12 width + $out .= sprintf("%11.11s",$$item{'amount_pretty'}) . ' '; #13 width + $out .= sprintf("%11.11s",$$item{'balance_pretty'}) . ' '; #13 width + $out .= sprintf("%.42s",$$item{'description'}); #max 42 width + $out .= "\n"; + } + return $out; +} + =back =head1 CLASS METHODS diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index bdad511fa..83ca3a27c 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -394,6 +394,11 @@ HTML body Text body +=item sub_param + +Optional list of parameter hashrefs to be passed +along to L. + =back Returns an error message, or false for success. @@ -470,6 +475,8 @@ sub email_search_result { 'cust_main' => $cust_main, 'object' => $obj, ); + $message{'sub_param'} = $param->{'sub_param'} + if $param->{'sub_param'}; } else { my @to = $cust_main->invoicing_list_emailonly; @@ -547,7 +554,9 @@ sub process_email_search_result { $param->{'search'} = thaw(decode_base64($param->{'search'})) or die "process_email_search_result requires search params.\n"; - + $param->{'sub_param'} = thaw(decode_base64($param->{'sub_param'})) + or die "process_email_search_result error decoding sub_param\n" + if $param->{'sub_param'}; # $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ] # unless ref($param->{'payby'}); diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index c52b6336e..fe8cbebb3 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -269,7 +269,19 @@ invoicing_list addresses. Multiple addresses may be comma-separated. =item substitutions -A hash reference of additional substitutions +A hash reference of additional string substitutions + +=item sub_param + +A hash reference, keys are the names of existing substitutions, +values are an addition parameter object to pass to the subroutine +for that substitution, e.g. + + 'sub_param' => { + 'payment_history' => { + 'start_date' => 1434764295, + }, + }, =back @@ -327,7 +339,10 @@ sub prepare { } elsif( ref($name) eq 'ARRAY' ) { # [ foo => sub { ... } ] - $hash{$prefix.($name->[0])} = $name->[1]->($obj); + my @subparam = (); + push(@subparam, $opt{'sub_param'}->{$name->[0]}) + if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]}; + $hash{$prefix.($name->[0])} = $name->[1]->($obj,@subparam); } else { warn "bad msg_template substitution: '$name'\n"; @@ -340,7 +355,10 @@ sub prepare { $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}}; } - $_ = encode_entities($_ || '') foreach values(%hash); + foreach my $key (keys %hash) { + next if $self->no_encode($key); + $hash{$key} = encode_entities($_ || ''); + }; ### # clean up template @@ -509,6 +527,13 @@ my $usage_warning = sub { #my $conf = new FS::Conf; +# for substitutions that handle their own encoding +sub no_encode { + my $self = shift; + my $field = shift; + return ($field eq 'payment_history'); +} + #return contexts and fill-in values # If you add anything, be sure to add a description in # httemplate/edit/msg_template.html. @@ -567,6 +592,12 @@ sub substitutions { [ selfservice_server_base_url => sub { $conf->config('selfservice_server-base_url') #, shift->agentnum) } ], + [ payment_history => sub { + my $cust_main = shift; + my $param = shift || {}; + #html works, see no_encode method + return '
' . encode_entities($cust_main->payment_history_text($param)) . '
'; + } ], ], # next_bill_date 'cust_pkg' => [qw( diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm index a4e27fdc9..87c407c35 100644 --- a/FS/FS/msg_template/InitialData.pm +++ b/FS/FS/msg_template/InitialData.pm @@ -18,6 +18,15 @@ If you did not request this password reset, you may safely ignore and delete thi

{ $company_name } Support +END + ], + }, + { msgname => 'payment_history_template', + mime_type => 'text/html', + _conf => 'payment_history_msgnum', + _insert_args => [ subject => '{ $company_name } payment history', + body => <<'END', +{ $payment_history } END ], }, -- cgit v1.2.1 From 4d00ce5afd7047c000a917f0fbd25ef7a7934f97 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 22 Jun 2015 23:43:00 -0500 Subject: RT#24665: Changes on the Redirect Letter [fixed conf names] --- FS/FS/Conf.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index f3e244777..58c966f84 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1306,7 +1306,7 @@ sub reason_type_options { }, { - 'key' => 'invoicehtmlwatermark', + 'key' => 'invoice_htmlwatermark', 'section' => 'invoicing', 'description' => 'Watermark for HTML invoices. Appears in a semitransparent positioned DIV overlaid on the main invoice container.', 'type' => 'textarea', @@ -1502,7 +1502,7 @@ and customer address. Include units.', }, { - 'key' => 'invoicelatexwatermark', + 'key' => 'invoice_latexwatermark', 'section' => 'invoicing', 'description' => 'Watermark for LaTeX invoices. See "texdoc background" for information on what this can contain. The content itself should be enclosed in braces, optionally followed by a comma and any formatting options.', 'type' => 'textarea', -- cgit v1.2.1 From 9bd28ce9e715ea971b8001a8320c7d1033347888 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 23 Jun 2015 00:42:06 -0500 Subject: RT#29895: Send email when backup is completed --- FS/FS/Conf.pm | 18 ++++++++++++++---- FS/FS/Cron/backup.pm | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 11 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 58c966f84..29d993e94 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -705,6 +705,11 @@ sub reason_type_options { } } +my $validate_email = sub { $_[0] =~ + /^[^@]+\@[[:alnum:]-]+(\.[[:alnum:]-]+)+$/ + ? '' : 'Invalid email address'; + }; + #Billing (81 items) #Invoicing (50 items) #UI (69 items) @@ -1196,10 +1201,7 @@ sub reason_type_options { 'description' => 'Return address on email invoices (address only, see invoice_from_name)', 'type' => 'text', 'per_agent' => 1, - 'validate' => sub { $_[0] =~ - /^[^@]+\@[[:alnum:]-]+(\.[[:alnum:]-]+)+$/ - ? '' : 'Invalid email address'; - } + 'validate' => $validate_email, }, { @@ -2764,6 +2766,14 @@ and customer address. Include units.', 'type' => 'text', }, + { + 'key' => 'dump-email_to', + 'section' => '', + 'description' => "Optional email address to send success/failure message for database dumps.", + 'type' => 'text', + 'validate' => $validate_email, + }, + { 'key' => 'users-allow_comp', 'section' => 'deprecated', diff --git a/FS/FS/Cron/backup.pm b/FS/FS/Cron/backup.pm index 5feca2636..cfc8e3624 100644 --- a/FS/FS/Cron/backup.pm +++ b/FS/FS/Cron/backup.pm @@ -6,6 +6,7 @@ use Exporter; use File::Copy; use Date::Format; use FS::UID qw(driver_name datasrc); +use FS::Misc qw( send_email ); @ISA = qw( Exporter ); @EXPORT_OK = qw( backup ); @@ -18,7 +19,8 @@ sub backup { my $filename = time2str('%Y%m%d%H%M%S',time); - datasrc =~ /dbname=([\w\.]+)$/ or die "unparsable datasrc ". datasrc; + datasrc =~ /dbname=([\w\.]+)$/ + or backup_email_and_die($conf,$filename,"unparsable datasrc ". datasrc); my $database = $1; my $ext; @@ -29,36 +31,71 @@ sub backup { system("mysqldump $database >/var/tmp/$database.sql"); $ext = 'sql'; } else { - die "database dumps not yet supported for ". driver_name; + backup_email_and_die($conf,$filename,"database dumps not yet supported for ". driver_name); } chmod 0600, "/var/tmp/$database.$ext"; if ( $conf->config('dump-pgpid') ) { eval 'use GnuPG;'; - die $@ if $@; + backup_email_and_die($conf,$filename,$@) if $@; my $gpg = new GnuPG; $gpg->encrypt( plaintext => "/var/tmp/$database.$ext", output => "/var/tmp/$database.gpg", recipient => $conf->config('dump-pgpid'), ); - unlink "/var/tmp/$database.$ext" or die $!; + unlink "/var/tmp/$database.$ext" + or backup_email_and_die($conf,$filename,$!); chmod 0600, "/var/tmp/$database.gpg"; $ext = 'gpg'; } if ( $localdest ) { - copy("/var/tmp/$database.$ext", "$localdest/$filename.$ext") or die $!; + copy("/var/tmp/$database.$ext", "$localdest/$filename.$ext") + or backup_email_and_die($conf,$filename,$!); chmod 0600, "$localdest/$filename.$ext"; } if ( $scpdest ) { eval "use Net::SCP qw(scp);"; - die $@ if $@; + backup_email_and_die($conf,$filename,$@) if $@; scp("/var/tmp/$database.$ext", "$scpdest/$filename.$ext"); } - unlink "/var/tmp/$database.$ext" or die $!; + unlink "/var/tmp/$database.$ext" or backup_email_and_die($conf,$filename,$!); #or just warn? + backup_email($conf,$filename); + +} + +#runs backup_email and dies with same error message +sub backup_email_and_die { + my ($conf,$filename,$error) = @_; + backup_email($conf,$filename,$error); + warn "backup_email_and_die called without error message" unless $error; + die $error; +} + +#checks if email should be sent, sends it +sub backup_email { + my ($conf,$filename,$error) = @_; + my $to = $conf->config('dump-email_to'); + return unless $to; + my $result = $error ? 'FAILED' : 'succeeded'; + my $email_error = send_email( + 'from' => $conf->config('invoice_from'), #or whatever, don't think it matters + 'to' => $to, + 'subject' => 'FREESIDE NOTIFICATION: Backup ' . $result, + 'body' => [ + "This is an automatic message from your Freeside installation.\n", + "Freeside backup $filename $result", + ($error ? " with the following error:\n\n" : "\n"), + ($error || ''), + "\n", + ], + 'msgtype' => 'admin', + ); + warn $email_error if $email_error; + return; } 1; -- cgit v1.2.1 From 3b46d452696901ff2dec41125f68c689ecffd5b9 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 25 Jun 2015 00:51:02 -0500 Subject: RT#34078: Payment History Report / Statement [refactor to not use msg_template] --- FS/FS/Conf.pm | 7 ------- FS/FS/cust_main.pm | 27 +-------------------------- FS/FS/cust_main_Mixin.pm | 11 +---------- FS/FS/msg_template.pm | 37 +++---------------------------------- FS/FS/msg_template/InitialData.pm | 9 --------- 5 files changed, 5 insertions(+), 86 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 29d993e94..238622bbd 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2656,13 +2656,6 @@ and customer address. Include units.', 'select_enum' => [ 'text/plain', 'text/html' ], }, - { - 'key' => 'payment_history_msgnum', - 'section' => 'notification', - 'description' => 'Template to use for sending payment history to customer', - %msg_template_options, - }, - { 'key' => 'payby', 'section' => 'billing', diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index b7efa180f..f2c2b4ab4 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4146,7 +4146,7 @@ I - optional already-loaded FS::Conf object. =cut # Caution: this gets used by FS::ClientAPI::MyAccount::billing_history, -# and also payment_history_text, which should both be kept customer-friendly. +# and also for sending customer statements, which should both be kept customer-friendly. # If you add anything that shouldn't be passed on through the API or exposed # to customers, add a new option to include it, don't include it by default sub payment_history { @@ -4268,31 +4268,6 @@ sub payment_history { return @out; } -=item payment_history_text - -Accepts the same options as L and returns those -results as a string table with fixed-width columns, max width 80 char. - -=cut - -sub payment_history_text { - my $self = shift; - my $opt = ref($_[0]) ? $_[0] : { @_ }; - my $out = sprintf("%-12s",'Date'); - $out .= sprintf("%11s",'Amount') . ' '; - $out .= sprintf("%11s",'Balance') . ' '; - $out .= 'Description'; #don't need to pad with spaces - $out .= "\n"; - foreach my $item ($self->payment_history($opt)) { - $out .= sprintf("%-10.10s",$$item{'date_pretty'}) . ' '; #12 width - $out .= sprintf("%11.11s",$$item{'amount_pretty'}) . ' '; #13 width - $out .= sprintf("%11.11s",$$item{'balance_pretty'}) . ' '; #13 width - $out .= sprintf("%.42s",$$item{'description'}); #max 42 width - $out .= "\n"; - } - return $out; -} - =back =head1 CLASS METHODS diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 83ca3a27c..bdad511fa 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -394,11 +394,6 @@ HTML body Text body -=item sub_param - -Optional list of parameter hashrefs to be passed -along to L. - =back Returns an error message, or false for success. @@ -475,8 +470,6 @@ sub email_search_result { 'cust_main' => $cust_main, 'object' => $obj, ); - $message{'sub_param'} = $param->{'sub_param'} - if $param->{'sub_param'}; } else { my @to = $cust_main->invoicing_list_emailonly; @@ -554,9 +547,7 @@ sub process_email_search_result { $param->{'search'} = thaw(decode_base64($param->{'search'})) or die "process_email_search_result requires search params.\n"; - $param->{'sub_param'} = thaw(decode_base64($param->{'sub_param'})) - or die "process_email_search_result error decoding sub_param\n" - if $param->{'sub_param'}; + # $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ] # unless ref($param->{'payby'}); diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index fe8cbebb3..c52b6336e 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -269,19 +269,7 @@ invoicing_list addresses. Multiple addresses may be comma-separated. =item substitutions -A hash reference of additional string substitutions - -=item sub_param - -A hash reference, keys are the names of existing substitutions, -values are an addition parameter object to pass to the subroutine -for that substitution, e.g. - - 'sub_param' => { - 'payment_history' => { - 'start_date' => 1434764295, - }, - }, +A hash reference of additional substitutions =back @@ -339,10 +327,7 @@ sub prepare { } elsif( ref($name) eq 'ARRAY' ) { # [ foo => sub { ... } ] - my @subparam = (); - push(@subparam, $opt{'sub_param'}->{$name->[0]}) - if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]}; - $hash{$prefix.($name->[0])} = $name->[1]->($obj,@subparam); + $hash{$prefix.($name->[0])} = $name->[1]->($obj); } else { warn "bad msg_template substitution: '$name'\n"; @@ -355,10 +340,7 @@ sub prepare { $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}}; } - foreach my $key (keys %hash) { - next if $self->no_encode($key); - $hash{$key} = encode_entities($_ || ''); - }; + $_ = encode_entities($_ || '') foreach values(%hash); ### # clean up template @@ -527,13 +509,6 @@ my $usage_warning = sub { #my $conf = new FS::Conf; -# for substitutions that handle their own encoding -sub no_encode { - my $self = shift; - my $field = shift; - return ($field eq 'payment_history'); -} - #return contexts and fill-in values # If you add anything, be sure to add a description in # httemplate/edit/msg_template.html. @@ -592,12 +567,6 @@ sub substitutions { [ selfservice_server_base_url => sub { $conf->config('selfservice_server-base_url') #, shift->agentnum) } ], - [ payment_history => sub { - my $cust_main = shift; - my $param = shift || {}; - #html works, see no_encode method - return '
' . encode_entities($cust_main->payment_history_text($param)) . '
'; - } ], ], # next_bill_date 'cust_pkg' => [qw( diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm index 87c407c35..a4e27fdc9 100644 --- a/FS/FS/msg_template/InitialData.pm +++ b/FS/FS/msg_template/InitialData.pm @@ -18,15 +18,6 @@ If you did not request this password reset, you may safely ignore and delete thi

{ $company_name } Support -END - ], - }, - { msgname => 'payment_history_template', - mime_type => 'text/html', - _conf => 'payment_history_msgnum', - _insert_args => [ subject => '{ $company_name } payment history', - body => <<'END', -{ $payment_history } END ], }, -- cgit v1.2.1 From 725118b4ae55c06cefb2d0b2ef41c7addf4a3a97 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 25 Jun 2015 10:33:06 -0700 Subject: padding with lines of all 9999s to blocks of 10 lines, RT#33622 --- FS/FS/pay_batch/nacha.pm | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm index c8d784d70..0361304e3 100644 --- a/FS/FS/pay_batch/nacha.pm +++ b/FS/FS/pay_batch/nacha.pm @@ -174,6 +174,15 @@ $DEBUG = 0; my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7); + my $lines = $batchcount + 4; + my $blocks = int($lines/10); + my $fill = ''; + + if ( my $remainder = $lines % 10 ) { + $blocks++; + $fill = ("\n".('9'x94))x( 10 - $remainder ); + } + warn "building Batch & File Control Records\n" if $DEBUG; ### @@ -199,12 +208,18 @@ $DEBUG = 0; '9'. #Record Type Code '000001'. #Batch Counter (# of batch header recs) - sprintf('%06d', $batchcount + 4). #num of physical blocks on the file..? + sprintf('%06d', $blocks). #num of physical blocks on the file sprintf('%08d', $batchcount). #total # of entry detail and addenda $entry_hash. sprintf('%012.0f', $batchtotal * 100). #Debit total '000000000000'. #Credit total - ( ' 'x39 ) #Reserved / blank + ( ' 'x39 ). #Reserved / blank + + ### + # Pad with 9999 records to blocks of 10 + ### + + $fill }, -- cgit v1.2.1 From 3ae0411e8c7d7ff14db95c0d9d9e3834ba431933 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 25 Jun 2015 20:59:54 -0500 Subject: RT#23741 Multiple payment options [selfservice paytypes fix] --- FS/FS/ClientAPI/MyAccount.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 610754c7a..9c323a4c3 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -48,6 +48,7 @@ use FS::msg_template; use FS::contact; use FS::cust_contact; use FS::cust_location; +use FS::cust_payby; use FS::ClientAPI::MyAccount::quotation; # just for code organization @@ -854,7 +855,7 @@ sub payment_info { 'require_cvv' => $conf->exists('selfservice-require_cvv'), 'onfile_require_cvv' => $conf->exists('selfservice-onfile_require_cvv'), - 'paytypes' => [ @FS::cust_main::paytypes ], + 'paytypes' => [ FS::cust_payby::paytypes ], 'paybys' => [ $conf->config('signup_server-payby') ], 'cust_paybys' => \@cust_paybys, -- cgit v1.2.1 From 9db30264ee0ce0f05632c1ed0b3e8dbdd03f7bc2 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 26 Jun 2015 01:08:34 -0500 Subject: RT#24684: Payments for Online Bill Pay --- FS/FS/ClientAPI/MyAccount.pm | 2 +- FS/FS/Conf.pm | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 9c323a4c3..85f85def9 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -131,7 +131,7 @@ sub skin_info { ), 'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ], ( map { $_ => $conf->exists("selfservice-$_", $agentnum ) } - qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo ) + qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo enable_payment_without_balance ) ), ( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) } qw( title_left_image title_right_image diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 238622bbd..17030d20c 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -5825,6 +5825,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'selfservice-enable_payment_without_balance', + 'section' => 'self-service', + 'description' => 'Allow selfservice customers to make payments even if balance is zero or below (resulting in an unapplied payment and negative balance.)', + 'type' => 'checkbox', + }, + { 'key' => 'logout-timeout', 'section' => 'UI', -- cgit v1.2.1 From 230ee8b52757cbd3aa45c47f0633a64809e704b8 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 26 Jun 2015 15:55:56 -0700 Subject: only try to log activity when there's an authenticated user, #36856 --- FS/FS/access_user_log.pm | 2 ++ 1 file changed, 2 insertions(+) (limited to 'FS') diff --git a/FS/FS/access_user_log.pm b/FS/FS/access_user_log.pm index 884d250d6..9e7f7a00e 100644 --- a/FS/FS/access_user_log.pm +++ b/FS/FS/access_user_log.pm @@ -75,6 +75,8 @@ Adds a log entry for PATH for the current user and timestamp. sub insert_new_path { my( $class, $path ) = @_; + return '' unless defined $FS::CurrentUser::CurrentUser; + my $self = $class->new( { 'usernum' => $FS::CurrentUser::CurrentUser->usernum, 'path' => $path, -- cgit v1.2.1 From 68546df9b125f73764eda31f1dcb4e2c0555f859 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 26 Jun 2015 19:26:17 -0400 Subject: more strict limits on tax-on-tax applicability, #36830 --- FS/FS/TaxEngine/cch.pm | 98 +++++++++++++++++++++++++++----------------------- FS/FS/tax_rate.pm | 5 ++- 2 files changed, 56 insertions(+), 47 deletions(-) (limited to 'FS') diff --git a/FS/FS/TaxEngine/cch.pm b/FS/FS/TaxEngine/cch.pm index fb3410365..ccfb846fe 100644 --- a/FS/FS/TaxEngine/cch.pm +++ b/FS/FS/TaxEngine/cch.pm @@ -123,7 +123,7 @@ sub make_taxlines { my @raw_taxlines; my %taxable_location; # taxable billpkgnum => cust_location - my %item_has_tax; # taxable billpkgnum => taxnum + my %item_has_tax; # taxable billpkgnum => charge class => taxnum foreach my $taxnum ( keys %{ $self->{taxes} } ) { my $tax_rate = FS::tax_rate->by_key($taxnum); my $taxables = $self->{taxes}{$taxnum}; @@ -141,8 +141,8 @@ sub make_taxlines { # store this tax fragment, indexed by taxable item, then by taxnum my $billpkgnum = $link->taxable_billpkgnum; - $item_has_tax{$billpkgnum} ||= {}; - my $fragments = $item_has_tax{$billpkgnum}{$taxnum} ||= []; + my $fragments = $item_has_tax{$billpkgnum}{$link->taxclass}{$taxnum} + ||= []; push @raw_taxlines, $link; # this will go into final consolidation push @$fragments, $link; # this will go into a temporary cust_bill_pkg @@ -156,48 +156,58 @@ sub make_taxlines { # taxes that apply to this item my $this_has_tax = $item_has_tax{$billpkgnum}; my $location = $taxable_location{$billpkgnum}; - foreach my $taxnum (keys %$this_has_tax) { - # $this_has_tax->{$taxnum} = an arrayref of the tax links for taxdef - # $taxnum on taxable item $billpkgnum - - my $tax_rate = FS::tax_rate->by_key($taxnum); - # find all taxes that apply to it in this location - my @tot = $tax_rate->tax_on_tax( $location ); - next if !@tot; - - warn "found possible taxed taxnum $taxnum\n" - if $DEBUG > 2; - # Calculate ToT separately for each taxable item, and only if _that - # item_ is already taxed under the ToT. This is counterintuitive. - # See RT#5243. - my $temp_lineitem; - foreach my $tot (@tot) { - my $totnum = $tot->taxnum; - warn "checking taxnum ".$tot->taxnum. - " which we call ". $tot->taxname ."\n" + + foreach my $charge_class (keys %$this_has_tax) { + # taxes that apply to this item and charge class + my $this_class_has_tax = $this_has_tax->{$charge_class}; + foreach my $taxnum (keys %$this_class_has_tax) { + + my $tax_rate = FS::tax_rate->by_key($taxnum); + # find all taxes that apply to it in this location + my @tot = $tax_rate->tax_on_tax( $location ); + next if !@tot; + + warn "found possible taxed taxnum $taxnum\n" if $DEBUG > 2; - if ( exists $this_has_tax->{ $totnum } ) { - warn "calculating tax on tax: taxnum ".$tot->taxnum." on $taxnum\n" - if $DEBUG; - # construct a line item to calculate tax on - $temp_lineitem ||= FS::cust_bill_pkg->new({ - 'pkgnum' => 0, - 'invnum' => $cust_bill->invnum, - 'setup' => sum(map $_->amount, @{ $this_has_tax->{$taxnum} }), - 'recur' => 0, - 'itemdesc' => $tax_rate->taxname, - 'cust_bill_pkg_tax_rate_location' => $this_has_tax->{$taxnum}, - }); - my @new_taxlines = $tot->taxline_cch( [ $temp_lineitem ] ); - next if (!@new_taxlines); # it didn't apply after all - if (!ref($new_taxlines[0])) { - die "error evaluating TOT ($totnum on $taxnum): $new_taxlines[0]\n"; - } - # add these to the taxline queue - push @raw_taxlines, @new_taxlines; - } # if $this_has_tax->{$totnum} - } # foreach my $tot (tax-on-tax rate definition) - } # foreach $taxnum (first-tier rate definition) + # Calculate ToT separately for each taxable item and class, and only + # if _that class on the item_ is already taxed under the ToT. This is + # counterintuitive. + # See RT#5243 and RT#36380. + my $temp_lineitem; + foreach my $tot (@tot) { + my $totnum = $tot->taxnum; + warn "checking taxnum ".$tot->taxnum. + " which we call ". $tot->taxname ."\n" + if $DEBUG > 2; + # note: if the _null class_ on this item is taxed under the ToT, + # then this specific class is taxed also (because null class + # includes all classes) and so ToT is applicable. + if ( + exists $this_class_has_tax->{ $totnum } + or exists $this_has_tax->{''}{ $totnum } + ) { + warn "calculating tax on tax: taxnum ".$tot->taxnum." on $taxnum\n" + if $DEBUG; + # construct a line item to calculate tax on + $temp_lineitem ||= FS::cust_bill_pkg->new({ + 'pkgnum' => 0, + 'invnum' => $cust_bill->invnum, + 'setup' => sum(map $_->amount, @{ $this_class_has_tax->{$taxnum} }), + 'recur' => 0, + 'itemdesc' => $tax_rate->taxname, + 'cust_bill_pkg_tax_rate_location' => $this_class_has_tax->{$taxnum}, + }); + my @new_taxlines = $tot->taxline_cch( [ $temp_lineitem ] ); + next if (!@new_taxlines); # it didn't apply after all + if (!ref($new_taxlines[0])) { + die "error evaluating TOT ($totnum on $taxnum): $new_taxlines[0]\n"; + } + # add these to the taxline queue + push @raw_taxlines, @new_taxlines; + } # if $this_has_tax->{$totnum} + } # foreach my $tot (tax-on-tax rate definition) + } # foreach $taxnum (first-tier rate definition) + } # foreach $charge_class } # foreach $taxable_item return @raw_taxlines; diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 67dd40e83..1094968c6 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -398,9 +398,6 @@ method together, and NO items from any other invoice should be included. =cut -# future optimization: it would probably suffice to return only the link -# records, and let the consolidation routine build the cust_bill_pkgs - sub taxline_cch { my $self = shift; # this used to accept a hash of options but none of them did anything @@ -581,8 +578,10 @@ sub taxline_cch { 'taxtype' => ref($self), 'cents' => $this_tax_cents, 'locationtaxid' => $self->location, + 'taxable_billpkgnum' => $cust_bill_pkg->billpkgnum, 'taxable_cust_bill_pkg' => $cust_bill_pkg, 'taxratelocationnum' => $taxratelocationnum, + 'taxclass' => $class, }); push @tax_links, $tax_link; -- cgit v1.2.1 From 062f38f2f5d0da64c6fd0702d4df2e805473a1f5 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 27 Jun 2015 15:51:29 -0700 Subject: reports with row grouping for payment/refund search, #25944 --- FS/FS/Mason.pm | 4 +- FS/FS/Query.pm | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 FS/FS/Query.pm (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 6163197fa..f05ac0339 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -57,7 +57,7 @@ if ( -e $addl_handler_use_file ) { use CGI::Cookie; use List::Util qw( max min sum ); use List::MoreUtils qw( first_index uniq ); - use Scalar::Util qw( blessed ); + use Scalar::Util qw( blessed looks_like_number ); use Data::Dumper; use Date::Format; use Time::Local; @@ -156,6 +156,8 @@ if ( -e $addl_handler_use_file ) { use FS::Locales; use FS::Maketext qw( mt emt js_mt ); + use FS::Query; + use FS::agent; use FS::agent_type; use FS::domain_record; diff --git a/FS/FS/Query.pm b/FS/FS/Query.pm new file mode 100644 index 000000000..8ecf1c49c --- /dev/null +++ b/FS/FS/Query.pm @@ -0,0 +1,118 @@ +package FS::Query; + +use strict; +use FS::Record; # don't import qsearch +use Storable 'dclone'; + +=head1 NAME + +FS::Query - A thin wrapper around qsearch argument hashes. + +=head1 DESCRIPTION + +This module exists because we pass qsearch argument lists around a lot, +and add new joins or WHERE expressions in several stages, and I got tired +of doing this: + + my $andwhere = "mycolumn IN('perl','python','javascript')"; + if ( ($search->{hashref} and keys( %{$search->{hashref}} )) + or $search->{extra_sql} =~ /^\s*WHERE/ ) { + $search->{extra_sql} .= " AND $andwhere"; + } else { + $search->{extra_sql} = " WHERE $andwhere "; + } + +and then having it fail under some conditions if it's done wrong (as the above +example is, obviously). + +We may eventually switch over to SQL::Abstract or something for this, but for +now it's a couple of crude manipulations and a wrapper to qsearch. + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Turns HASHREF (a qsearch argument list) into an FS::Query object. None of +the params are really required, but you should at least supply C. + +In the Future this may do a lot more stuff. + +=cut + +sub new { + my ($class, $hashref) = @_; + + my $self = bless { + table => '', + select => '*', + hashref => {}, + addl_from => '', + extra_sql => '', + order_by => '', + %$hashref, + }; + # load FS::$table? validate anything? + $self; +} + +=item clone + +Returns another object that's a copy of this one. + +=cut + +sub clone { + my $self = shift; + $self->new( dclone($self) ); +} + +=item and_where EXPR + +Adds a constraint to the WHERE clause of the query. All other constraints in +the WHERE clause should be joined with AND already; if not, they should be +grouped with parentheses. + +=cut + +sub and_where { + my $self = shift; + my $where = shift; + + if ($self->{extra_sql} =~ /^\s*(?:WHERE|AND)\s+(.*)/is) { + $where = "($where) AND $1"; + } + if (keys %{ $self->{hashref} }) { + $where = " AND $where"; + } else { + $where = " WHERE $where"; + } + $self->{extra_sql} = $where; + + return $self; +} + +=item qsearch + +Runs the query and returns all results. + +=cut + +sub qsearch { + my $self = shift; + FS::Record::qsearch({ %$self }); +} + +=item qsearchs + +Runs the query and returns only one result. + +=cut + +sub qsearchs { + my $self = shift; + FS::Record::qsearchs({ %$self }); +} + +1; -- cgit v1.2.1 From 1fa379f596dd95694ed4e37a19afd44d8e47e82a Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 29 Jun 2015 07:04:53 -0400 Subject: Ticket #36843 Fix incorrect call error --- FS/FS/cdr/broadsoft.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/cdr/broadsoft.pm b/FS/FS/cdr/broadsoft.pm index b5d75f13f..a6f4d01c0 100644 --- a/FS/FS/cdr/broadsoft.pm +++ b/FS/FS/cdr/broadsoft.pm @@ -39,7 +39,8 @@ use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker ); skip(17), sub { my($cdr, $accountcode) = @_; if ($cdr->is_tollfree){ - $cdr->set('accountcode', $cdr->dst); + my $dst = substr($cdr->dst,0,32); + $cdr->set('accountcode', $dst); } else { $cdr->set('accountcode', $accountcode); }}, -- cgit v1.2.1 From 18dac45495554fd8a28eab2a50f9af39953d37ca Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 29 Jun 2015 20:32:46 -0700 Subject: silence unnecessary but too-scary warning about CGI::param in list context --- FS/FS/Mason/Request.pm | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'FS') diff --git a/FS/FS/Mason/Request.pm b/FS/FS/Mason/Request.pm index 62bf670b9..2cf1ed9e0 100644 --- a/FS/FS/Mason/Request.pm +++ b/FS/FS/Mason/Request.pm @@ -111,6 +111,10 @@ sub freeside_setup { FS::Trace->log(' UTF-8-decoding form data'); # foreach my $param ( $cgi->param ) { + + #we can't switch to multi_param until we're done supporting deb 7 + local($CGI::LIST_CONTEXT_WARN) = 0; + my @values = $cgi->param($param); next if $cgi->uploadInfo($values[0]); #warn $param; -- cgit v1.2.1 From 633af1c0e996b16d0e18a7cdff05490ea7d46ca6 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 29 Jun 2015 21:21:53 -0700 Subject: setup+recur total on quotations, RT#36997 --- FS/FS/quotation.pm | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index 8843a8709..7bf5ecb61 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -260,15 +260,30 @@ sub _items_sections { my %opt = @_; my $escape = $opt{escape}; # the only one we care about - my %subtotals; # package frequency => subtotal + my %subtotals = (); # package frequency => subtotal + my $disable_total = 0; foreach my $pkg ($self->quotation_pkg) { - my $recur_freq = $pkg->part_pkg->freq; + + my $part_pkg = $pkg->part_pkg; + + my $recur_freq = $part_pkg->freq; ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax; ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax; + + #this is a shitty hack based on what's in part_pkg/ at the moment + # but its good enough for the 99% common case of preventing totals from + # displaying for prorate packages + $disable_total = 1 + if $part_pkg->plan =~ /^prorate/ + || $part_pkg->plan eq 'agent' + || $part_pkg->plan =~ /^torrus/ + || $part_pkg->option('sync_bill_date'); + } my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs }; my @sections; + my $no_recurring = 0; foreach my $freq (keys %subtotals) { next if $subtotals{$freq} == 0; @@ -279,6 +294,7 @@ sub _items_sections { if ( $freq eq '0' ) { if ( scalar(keys(%subtotals)) == 1 ) { # there are no recurring packages + $no_recurring = 1; $desc = $self->mt('Charges'); } else { $desc = $self->mt('Setup Charges'); @@ -295,6 +311,18 @@ sub _items_sections { 'subtotal' => sprintf('%.2f',$subtotals{$freq}), }; } + + unless ( $disable_total || $no_recurring ) { + my $total = 0; + $total += $_ for values %subtotals; + push @sections, { + 'description' => 'First payment', + 'sort_weight' => 0, + 'category' => 'Total category', #required but what's it used for? + 'subtotal' => sprintf('%.2f',$total) + }; + } + return \@sections, []; } -- cgit v1.2.1 From d13dae1c37c36c27f1ac9fd134c5d8b3a4fb9754 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 30 Jun 2015 03:24:08 -0500 Subject: RT#29285: State field not needed for New Zealand --- FS/FS/Conf.pm | 15 +++++----- FS/FS/Schema.pm | 4 +-- FS/FS/UI/Web/small_custview.pm | 5 ++-- FS/FS/cust_location.pm | 67 ++++++++++++++++++++++++++++++++++++++---- FS/FS/cust_pay_batch.pm | 6 +++- 5 files changed, 78 insertions(+), 19 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 17030d20c..b384d85af 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3382,14 +3382,6 @@ and customer address. Include units.', 'per_agent' => 1, }, - { - 'key' => 'city_not_required', - 'section' => 'required', - 'description' => 'Turn off requirement for a City to be entered for billing & shipping addresses', - 'type' => 'checkbox', - 'per_agent' => 1, - }, - { 'key' => 'echeck-void', 'section' => 'deprecated', @@ -4524,6 +4516,13 @@ and customer address. Include units.', 'type' => 'checkbox', }, + { + 'key' => 'cust_main-no_city_in_address', + 'section' => 'UI', + 'description' => 'Turn off City for billing & shipping addresses', + 'type' => 'checkbox', + }, + { 'key' => 'census_year', 'section' => 'UI', diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 0bdc99539..24ca85821 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2009,7 +2009,7 @@ sub tables_hashref { 'locationname', 'varchar', 'NULL', $char_d, '', '', 'address1', 'varchar', '', $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, '', '', @@ -2639,7 +2639,7 @@ sub tables_hashref { 'first', 'varchar', '', $char_d, '', '', 'address1', 'varchar', '', $char_d, '', '', 'address2', 'varchar', 'NULL', $char_d, '', '', - 'city', 'varchar', '', $char_d, '', '', + 'city', 'varchar', 'NULL', $char_d, '', '', 'state', 'varchar', 'NULL', $char_d, '', '', 'zip', 'varchar', 'NULL', 10, '', '', 'country', 'char', '', 2, '', '', diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm index 72af03ea4..329e5f7eb 100644 --- a/FS/FS/UI/Web/small_custview.pm +++ b/FS/FS/UI/Web/small_custview.pm @@ -129,7 +129,8 @@ sub small_custview { $html .= encode_entities($cust_main->address1). '
'; $html .= encode_entities($cust_main->address2). '
' if $cust_main->address2; - $html .= encode_entities($cust_main->city). ', '. $cust_main->state. ' '. + $html .= encode_entities($cust_main->city) . ', ' if $cust_main->city; + $html .= $cust_main->state. ' '. $cust_main->zip. '
'; $html .= $cust_main->country. '
' if $cust_main->country && $cust_main->country ne $countrydefault; @@ -162,7 +163,7 @@ sub small_custview { $cust_main->ship_company, $ship->address1, $ship->address2, - ($ship->city . ', ' . $ship->state . ' ' . $ship->zip), + (($ship->city ? $ship->city . ', ' : '') . $ship->state . ' ' . $ship->zip), ($ship->country eq $countrydefault ? '' : $ship->country ), ); diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index a863e5df7..9ab94f207 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -68,7 +68,7 @@ Address line two (optional) =item city -City +City (optional only if cust_main-no_city_in_address config is set) =item county @@ -149,9 +149,20 @@ sub find_or_insert { warn "find_or_insert:\n".Dumper($self) if $DEBUG; - my @essential = (qw(custnum address1 address2 city county state zip country + my @essential = (qw(custnum address1 address2 county state zip country location_number location_type location_kind disabled)); + # Just in case this conf was accidentally/temporarily set, + # we'll never overwrite existing city; see city method + if ($conf->exists('cust_main-no_city_in_address')) { + warn "Warning: find_or_insert specified city when cust_main-no_city_in_address was configured" + if $self->get('city'); + $self->set('city',''); # won't end up in %nonempty, hence old value is preserved + } else { + # otherwise, of course, city is essential + push(@essential,'city') + } + # I don't think this is necessary #if ( !$self->coord_auto and $self->latitude and $self->longitude ) { # push @essential, qw(latitude longitude); @@ -207,6 +218,11 @@ otherwise returns false. sub insert { my $self = shift; + # Ideally, this should never happen, + # but throw a warning and save the value anyway, to avoid data loss + warn "Warning: inserting city when cust_main-no_city_in_address is configured" + if $conf->exists('cust_main-no_city_in_address') && $self->get('city'); + if ( $self->censustract ) { $self->set('censusyear' => $conf->config('census_year') || 2012); } @@ -271,6 +287,15 @@ sub replace { my $self = shift; my $old = shift; $old ||= $self->replace_old; + + # Just in case this conf was accidentally/temporarily set, + # we'll never overwrite existing city; see city method + if ($conf->exists('cust_main-no_city_in_address')) { + warn "Warning: replace attempted to change city when cust_main-no_city_in_address was configured" + if $self->get('city') && ($old->get('city') != $self->get('city')); + $self->set('city',$old->get('city')); + } + # the following fields are immutable foreach (qw(address1 address2 city state zip country)) { if ( $self->$_ ne $old->$_ ) { @@ -330,7 +355,9 @@ sub check { || $self->ut_textn('locationname') || $self->ut_text('address1') || $self->ut_textn('address2') - || $self->ut_text('city') + || ($conf->exists('cust_main-no_city_in_address') + ? $self->ut_textn('city') + : $self->ut_text('city')) || $self->ut_textn('county') || $self->ut_textn('state') || $self->ut_country('country') @@ -392,6 +419,30 @@ sub check { $self->SUPER::check; } +=item city + +When the I config is set, the +city method will return a blank string no matter the previously +set value of the field. You can still use the get method to +access the contents of the field directly. + +Just in case this config was accidentally/temporarily set, +we'll never overwrite existing city while the config is active. +L will throw a warning if passed any true value for city, +ignore the city field when finding, and preserve the existing value. +L will only throw a warning if passed a true value that is +different than the existing value of city, and will preserve the existing value. +L will throw a warning but still insert a true city value, +to avoid unnecessary data loss. + +=cut + +sub city { + my $self = shift; + return '' if $conf->exists('cust_main-no_city_in_address'); + return $self->get('city'); +} + =item country_full Returns this locations's full country name @@ -731,25 +782,29 @@ names in order. =cut +### Is this actually used for anything anymore? Grep doesn't show anything... 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'); + unless ($conf->exists('cust_main-no_city_in_address')) { + push( @fields, (('city') x 3) ); + } + 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 = ?" + "cust_location.country = ?", + "cust_location.city = ? OR ? = '' OR CAST(? AS $text) IS NULL" ); my $sql = join(' AND ', map "($_)\n", @where); if ( $opt{param} ) { diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm index 13b2eefe5..8f31e4dda 100644 --- a/FS/FS/cust_pay_batch.pm +++ b/FS/FS/cust_pay_batch.pm @@ -129,6 +129,8 @@ and replace methods. sub check { my $self = shift; + my $conf = new FS::Conf; + my $error = $self->ut_numbern('paybatchnum') || $self->ut_numbern('trancode') #deprecated @@ -137,7 +139,9 @@ sub check { || $self->ut_number('custnum') || $self->ut_text('address1') || $self->ut_textn('address2') - || $self->ut_text('city') + || ($conf->exists('cust_main-no_city_in_address') + ? $self->ut_textn('city') + : $self->ut_text('city')) || $self->ut_textn('state') ; -- cgit v1.2.1 From cc577407362f8b64817afbe89d23888a0a5b63f9 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 30 Jun 2015 05:29:49 -0500 Subject: RT#30705: Change contract end date when changing packages [got rid of chronology requirements] --- FS/FS/cust_pkg.pm | 3 --- 1 file changed, 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 5bd307b0f..8e8872871 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1986,9 +1986,6 @@ sub _check_change { #option shouldn't be passed, throw error if it's non-empty return "Cannot add contract end date when changing packages " . $self->pkgnum; } - if ($opt->{'start_date'} && ($opt->{'contract_end'} < $opt->{'start_date'})) { - return "Contract end date is before change date"; - } } return ''; } -- cgit v1.2.1 From 39501b4ff6c6cd9ac3cfe7ad313affe049e18994 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 30 Jun 2015 12:14:19 -0700 Subject: include discounts in gross sales reports, #25943 --- FS/FS/Report/Table.pm | 30 ++++++++++++++++ FS/FS/cust_bill/Search.pm | 91 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 95 insertions(+), 26 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 479747307..eef983d68 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -266,6 +266,36 @@ sub netrefunds { ); } +=item discounted: The sum of discounts on invoices in the period. + +=cut + +sub discounted { + my( $self, $speriod, $eperiod, $agentnum, %opt) = @_; + $self->scalar_sql('SELECT SUM(cust_bill_pkg_discount.amount) + FROM cust_bill_pkg_discount + JOIN cust_bill_pkg USING ( billpkgnum ) + JOIN cust_bill USING ( invnum ) + JOIN cust_main USING ( custnum ) + WHERE '. $self->in_time_period_and_agent( $speriod, + $eperiod, + $agentnum, + 'cust_bill._date' + ). + $self->for_opts(%opt) + ); +} + +=item gross: invoiced + discounted + +=cut + +sub gross { + my( $self, $speriod, $eperiod, $agentnum, %opt) = @_; + $self->invoiced( $speriod, $eperiod, $agentnum, %opt) + + $self->discounted( $speriod, $eperiod, $agentnum, %opt); +} + #XXX docs #these should be auto-generated or $AUTOLOADed or something diff --git a/FS/FS/cust_bill/Search.pm b/FS/FS/cust_bill/Search.pm index 2a67529c1..62c55d6df 100644 --- a/FS/FS/cust_bill/Search.pm +++ b/FS/FS/cust_bill/Search.pm @@ -6,14 +6,15 @@ use FS::UI::Web; use FS::Record qw( qsearchs dbh ); use FS::cust_main; use FS::access_user; +use FS::Conf; =item search HASHREF (Class method) -Returns a qsearch hash expression to search for parameters specified in HASHREF. -In addition to all parameters accepted by search_sql_where, the following -additional parameters valid: +Returns a qsearch hash expression to search for parameters specified in +HASHREF. In addition to all parameters accepted by search_sql_where, the +following additional parameters valid: =over 4 @@ -38,35 +39,77 @@ sub search { my $join_cust_main = FS::UI::Web::join_cust_main('cust_bill'); + # get discounted, credited, and paid amounts here, for use in report + # + # Testing shows that this is by far the most efficient way to do the + # joins. In particular it's almost 100x faster to join to an aggregate + # query than to put the subquery in a select expression. It also makes + # it more convenient to do arithmetic between columns, use them as sort + # keys, etc. + # + # Each ends with a RIGHT JOIN cust_bill so that it includes all invnums, + # even if they have no discounts/credits/payments; the total amount is then + # coalesced to zero. + my $join = "$join_cust_main + JOIN ( + SELECT COALESCE(SUM(cust_bill_pkg_discount.amount), 0) AS discounted, + invnum + FROM cust_bill_pkg_discount + JOIN cust_bill_pkg USING (billpkgnum) + RIGHT JOIN cust_bill USING (invnum) + GROUP BY invnum + ) AS _discount USING (invnum) + JOIN ( + SELECT COALESCE(SUM(cust_credit_bill.amount), 0) AS credited, invnum + FROM cust_credit_bill + RIGHT JOIN cust_bill USING (invnum) + GROUP BY invnum + ) AS _credit USING (invnum) + JOIN ( + SELECT COALESCE(SUM(cust_bill_pay.amount), 0) AS paid, invnum + FROM cust_bill_pay + RIGHT JOIN cust_bill USING (invnum) + GROUP BY invnum + ) AS _pay USING (invnum) + "; + unless ( $count_query ) { + + my $money = (FS::Conf->new->config('money_char') || '$') . '%.2f'; + $count_query = 'SELECT COUNT(*), '. join(', ', map "SUM($_)", - ( 'charged', - FS::cust_bill->net_sql, - FS::cust_bill->owed_sql, + ( 'charged + discounted', + 'discounted', + 'credited', + 'charged - credited', + 'charged - credited - paid', ) ); - $count_addl = [ '$%.2f invoiced (gross)', - '$%.2f invoiced (net)', - '$%.2f outstanding balance', + $count_addl = [ "$money sales (gross)", + "− $money discounted", + "− $money credited", + "= $money sales (net)", + "$money outstanding balance", ]; } - $count_query .= " FROM cust_bill $join_cust_main $extra_sql"; + $count_query .= " FROM cust_bill $join $extra_sql"; #$sql_query = +{ 'table' => 'cust_bill', - 'addl_from' => $join_cust_main, + 'addl_from' => $join, 'hashref' => {}, 'select' => join(', ', 'cust_bill.*', #( map "cust_main.$_", qw(custnum last first company) ), 'cust_main.custnum as cust_main_custnum', FS::UI::Web::cust_sql_fields(), - #$class->owed_sql. ' AS owed', - #$class->net_sql. ' AS net', - FS::cust_bill->owed_sql. ' AS owed', - FS::cust_bill->net_sql. ' AS net', + '(charged + discounted) as gross', + 'discounted', + 'credited', + '(charged - credited) as net', + '(charged - credited - paid) as owed', ), 'extra_sql' => $extra_sql, 'order_by' => 'ORDER BY '. ( $params->{'order_by'} || 'cust_bill._date' ), @@ -179,13 +222,7 @@ sub search_sql_where { @classnum = grep /^(\d*)$/, @classnum; if ( @classnum ) { - push @search, '( '. join(' OR ', map { - $_ ? "cust_main.classnum = $_" - : "cust_main.classnum IS NULL" - } - @classnum - ). - ' )'; + push @search, 'COALESCE(cust_main.classnum, 0) IN ('.join(',', @classnum).')'; } } @@ -206,6 +243,7 @@ sub search_sql_where { push @search, "cust_bill.invnum <= $1"; } + # these are from parse_lt_gt, and should already be sanitized #charged if ( $param->{charged} ) { my @charged = ref($param->{charged}) @@ -216,21 +254,22 @@ sub search_sql_where { @charged; } - my $owed_sql = FS::cust_bill->owed_sql; + #my $owed_sql = FS::cust_bill->owed_sql; + my $owed_sql = '(cust_bill.charged - credited - paid)'; + my $net_sql = '(cust_bill.charged - credited)'; #owed if ( $param->{owed} ) { my @owed = ref($param->{owed}) ? @{ $param->{owed} } : ($param->{owed}); - push @search, map { s/^owed/$owed_sql/; $_; } - @owed; + push @search, map { s/^owed/$owed_sql/ } @owed; } #open/net flags push @search, "0 != $owed_sql" if $param->{'open'}; - push @search, '0 != '. FS::cust_bill->net_sql + push @search, "0 != $net_sql" if $param->{'net'}; #days -- cgit v1.2.1 From b02738fceb2de16b23fb22a70350b19ee88a214c Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 30 Jun 2015 22:30:56 -0500 Subject: RT#20687: Future package change [bug fix] --- FS/FS/cust_pkg.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 8e8872871..b97587c2c 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2442,8 +2442,9 @@ sub change_later { $error = $self->replace || $err_or_pkg->replace || - $change_to->cancel('no_delay_cancel' => 1) || - $change_to->delete; + (($err_or_pkg->pkgnum == $change_to->pkgnum) ? '' : + $change_to->cancel('no_delay_cancel' => 1) || + $change_to->delete); } else { $error = $err_or_pkg; } -- cgit v1.2.1 From 8f57cf37b51aefcdbf13fe09291611f1e8c08650 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 30 Jun 2015 22:49:28 -0500 Subject: RT#20687: Future package change [comment about bug fix] --- FS/FS/cust_pkg.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'FS') diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index b97587c2c..950d348ce 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -2442,6 +2442,7 @@ sub change_later { $error = $self->replace || $err_or_pkg->replace || + #because change() might've edited existing scheduled change in place (($err_or_pkg->pkgnum == $change_to->pkgnum) ? '' : $change_to->cancel('no_delay_cancel' => 1) || $change_to->delete); -- cgit v1.2.1 From 1004de872b685fefcc543f16b5c0f23b289b9d80 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 1 Jul 2015 15:46:29 -0700 Subject: payment total on quotes: not when recur_method eq prorate, and be more refined about disabling with sync_bill-date --- FS/FS/quotation.pm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm index 7bf5ecb61..f82051066 100644 --- a/FS/FS/quotation.pm +++ b/FS/FS/quotation.pm @@ -274,10 +274,12 @@ sub _items_sections { # but its good enough for the 99% common case of preventing totals from # displaying for prorate packages $disable_total = 1 - if $part_pkg->plan =~ /^prorate/ - || $part_pkg->plan eq 'agent' - || $part_pkg->plan =~ /^torrus/ - || $part_pkg->option('sync_bill_date'); + if $part_pkg->plan =~ /^(prorate|torrus|agent$)/ + || $part_pkg->option('recur_method') eq 'prorate' + || ( $part_pkg->option('sync_bill_date') + && $self->custnum + && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it + ); } my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs }; -- cgit v1.2.1 From 2bcf13f0f9da22f9afa134320943ea2bfc9c7dcb Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 1 Jul 2015 17:57:44 -0500 Subject: RT#29285: State field not needed for New Zealand [bug fixes] --- FS/FS/UI/Web/small_custview.pm | 6 +-- FS/FS/cust_location.pm | 111 ++++------------------------------------- 2 files changed, 14 insertions(+), 103 deletions(-) (limited to 'FS') diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm index 329e5f7eb..a1173f7da 100644 --- a/FS/FS/UI/Web/small_custview.pm +++ b/FS/FS/UI/Web/small_custview.pm @@ -130,9 +130,9 @@ sub small_custview { $html .= encode_entities($cust_main->address2). '
' if $cust_main->address2; $html .= encode_entities($cust_main->city) . ', ' if $cust_main->city; - $html .= $cust_main->state. ' '. - $cust_main->zip. '
'; - $html .= $cust_main->country. '
' + $html .= encode_entities($cust_main->state). ' '. + encode_entities($cust_main->zip). '
'; + $html .= encode_entities($cust_main->country). '
' if $cust_main->country && $cust_main->country ne $countrydefault; } diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index 9ab94f207..7f4aa9a79 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -68,7 +68,7 @@ Address line two (optional) =item city -City (optional only if cust_main-no_city_in_address config is set) +City (if cust_main-no_city_in_address config is set when inserting, this will be forced blank) =item county @@ -149,18 +149,13 @@ sub find_or_insert { warn "find_or_insert:\n".Dumper($self) if $DEBUG; - my @essential = (qw(custnum address1 address2 county state zip country + my @essential = (qw(custnum address1 address2 city county state zip country location_number location_type location_kind disabled)); - # Just in case this conf was accidentally/temporarily set, - # we'll never overwrite existing city; see city method if ($conf->exists('cust_main-no_city_in_address')) { - warn "Warning: find_or_insert specified city when cust_main-no_city_in_address was configured" + warn "Warning: passed city to find_or_insert when cust_main-no_city_in_address is configured, ignoring it" if $self->get('city'); - $self->set('city',''); # won't end up in %nonempty, hence old value is preserved - } else { - # otherwise, of course, city is essential - push(@essential,'city') + $self->set('city',''); } # I don't think this is necessary @@ -218,10 +213,11 @@ otherwise returns false. sub insert { my $self = shift; - # Ideally, this should never happen, - # but throw a warning and save the value anyway, to avoid data loss - warn "Warning: inserting city when cust_main-no_city_in_address is configured" - if $conf->exists('cust_main-no_city_in_address') && $self->get('city'); + if ($conf->exists('cust_main-no_city_in_address')) { + warn "Warning: passed city to insert when cust_main-no_city_in_address is configured, ignoring it" + if $self->get('city'); + $self->set('city',''); + } if ( $self->censustract ) { $self->set('censusyear' => $conf->config('census_year') || 2012); @@ -288,13 +284,8 @@ sub replace { my $old = shift; $old ||= $self->replace_old; - # Just in case this conf was accidentally/temporarily set, - # we'll never overwrite existing city; see city method - if ($conf->exists('cust_main-no_city_in_address')) { - warn "Warning: replace attempted to change city when cust_main-no_city_in_address was configured" - if $self->get('city') && ($old->get('city') != $self->get('city')); - $self->set('city',$old->get('city')); - } + warn "Warning: passed city to replace when cust_main-no_city_in_address is configured" + if $conf->exists('cust_main-no_city_in_address') && $self->get('city'); # the following fields are immutable foreach (qw(address1 address2 city state zip country)) { @@ -419,30 +410,6 @@ sub check { $self->SUPER::check; } -=item city - -When the I config is set, the -city method will return a blank string no matter the previously -set value of the field. You can still use the get method to -access the contents of the field directly. - -Just in case this config was accidentally/temporarily set, -we'll never overwrite existing city while the config is active. -L will throw a warning if passed any true value for city, -ignore the city field when finding, and preserve the existing value. -L will only throw a warning if passed a true value that is -different than the existing value of city, and will preserve the existing value. -L will throw a warning but still insert a true city value, -to avoid unnecessary data loss. - -=cut - -sub city { - my $self = shift; - return '' if $conf->exists('cust_main-no_city_in_address'); - return $self->get('city'); -} - =item country_full Returns this locations's full country name @@ -766,62 +733,6 @@ sub county_state_country { =back -=head1 CLASS METHODS - -=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 - -### Is this actually used for anything anymore? Grep doesn't show anything... -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, - ('county') x $x, - ('state') x $x, - 'country'); - - unless ($conf->exists('cust_main-no_city_in_address')) { - push( @fields, (('city') x 3) ); - } - - my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text'; - - my @where = ( - "cust_location.district = ? 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 = ?", - "cust_location.city = ? OR ? = '' OR CAST(? AS $text) IS NULL" - ); - 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; - } -} - -=back - =head2 SUBROUTINES =over 4 -- cgit v1.2.1 From 0b9c15a3add1d3715c3895171894ac6480d1dfc1 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 2 Jul 2015 13:16:21 -0500 Subject: RT#18834: Cacti integration [added warning messages] --- FS/FS/part_export/cacti.pm | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'FS') diff --git a/FS/FS/part_export/cacti.pm b/FS/FS/part_export/cacti.pm index eff6c5220..b41fe9bc8 100644 --- a/FS/FS/part_export/cacti.pm +++ b/FS/FS/part_export/cacti.pm @@ -490,8 +490,12 @@ sub process_graphs { $dbh->rollback if $oldAutoCommit; die $error; } + } else { + warn "File $thumbfile is too large, skipping"; } unlink($thumbfile); + } else { + warn "File $thumbfile does not exist, skipping"; } $job->update_statustext(49 + int($i / @graphs) * 50); } -- cgit v1.2.1 From 632dd8ec5c4f4eaccb71607407b934ddf8935104 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 2 Jul 2015 13:36:27 -0500 Subject: RT#18834: Cacti integration [warnings display to screen] --- FS/FS/part_export/cacti.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_export/cacti.pm b/FS/FS/part_export/cacti.pm index b41fe9bc8..c83e453cd 100644 --- a/FS/FS/part_export/cacti.pm +++ b/FS/FS/part_export/cacti.pm @@ -491,11 +491,11 @@ sub process_graphs { die $error; } } else { - warn "File $thumbfile is too large, skipping"; + $svchtml .= qq(

File $thumbfile is too large, skipping

); } unlink($thumbfile); } else { - warn "File $thumbfile does not exist, skipping"; + $svchtml .= qq(

File $thumbfile does not exist, skipping

); } $job->update_statustext(49 + int($i / @graphs) * 50); } -- cgit v1.2.1 From 53b6529e6a9c3eb3a314d87e4a405b17af4daf45 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 3 Jul 2015 17:19:53 -0500 Subject: RT#18834: Cacti integration [trailing slash handling, changed cache dir] --- FS/FS/part_export/cacti.pm | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_export/cacti.pm b/FS/FS/part_export/cacti.pm index c83e453cd..7de97946c 100644 --- a/FS/FS/part_export/cacti.pm +++ b/FS/FS/part_export/cacti.pm @@ -217,7 +217,7 @@ sub ssh_insert { # $desc =~ s/'/'\\''/g; $desc =~ s/'//g; my $cmd = $php - . $opt{'script_path'} + . trailslash($opt{'script_path'}) . q(add_device.php --description=') . $desc . q(' --ip=') @@ -233,7 +233,7 @@ sub ssh_insert { # Add host to tree if ($opt{'tree_id'}) { $cmd = $php - . $opt{'script_path'} + . trailslash($opt{'script_path'}) . q(add_tree.php --type=node --node-type=host --tree-id=) . $opt{'tree_id'} . q( --host-id=) @@ -246,7 +246,7 @@ sub ssh_insert { # Get list of graph templates for new id $cmd = $php - . $opt{'script_path'} + . trailslash($opt{'script_path'}) . q(freeside_cacti.php --get-graph-templates --host-template=) . $opt{'template_id'}; my $ginfo = { map { $_ ? ($_ => undef) : () } split(/\n/,ssh_cmd(%opt, 'command' => $cmd)) }; @@ -301,7 +301,7 @@ sub ssh_insert { # create the graph $cmd = $php - . $opt{'script_path'} + . trailslash($opt{'script_path'}) . q(add_graphs.php --graph-type=) . ($isds ? 'ds' : 'cg') . q( --graph-template-id=) @@ -335,7 +335,7 @@ sub ssh_insert { sub ssh_delete { my %opt = @_; my $cmd = $php - . $opt{'script_path'} + . trailslash($opt{'script_path'}) . q(freeside_cacti.php --drop-device --ip=') . $opt{'hostname'} . q('); @@ -367,7 +367,7 @@ sub process_graphs { my ($job,$param) = @_; $job->update_statustext(10); - my $cachedir = $FS::UID::cache_dir . '/cacti-graphs/'; + my $cachedir = trailslash($FS::UID::cache_dir,'cache.'.$FS::UID::datasrc,'cacti-graphs'); # load the service my $svcnum = $param->{'svcnum'} || die "No svcnum specified"; @@ -413,7 +413,7 @@ sub process_graphs { # get list of graphs for this svc from cacti server my $cmd = $php - . $self->option('script_path') + . trailslash($self->option('script_path')) . q(freeside_cacti.php --get-graphs --ip=') . $svc->ip_addr . q('); @@ -432,7 +432,9 @@ sub process_graphs { 'rsh' => 'ssh', 'verbose' => 1, 'recursive' => 1, - 'source' => $self->option('graphs_path'), + 'quote-src' => 1, + 'quote-dst' => 1, + 'source' => trailslash($self->option('graphs_path')), 'dest' => $cachedir, 'include' => [ (map { q('**graph_).${$_}[0].q(*.png') } @graphs), @@ -442,8 +444,9 @@ sub process_graphs { ], }); #don't know why a regular $rsync->exec isn't doing includes right, but this does - my $error = system(join(' ',@{$rsync->getcmd()})); - die "rsync failed with exit status $error" if $error; + my $rscmd = join(' ',@{$rsync->getcmd()}); + my $error = system($rscmd); + die "rsync ($rscmd) failed with exit status $error" if $error; $job->update_statustext(50); @@ -551,6 +554,19 @@ sub ssh_cmd { return $output; } +#there's probably a better place to put this? +#makes sure there's a trailing slash between/after input +#doesn't add leading slashes +sub trailslash { + my @paths = @_; + my $out = ''; + foreach my $path (@paths) { + $out .= $path; + $out .= '/' unless $out =~ /\/$/; + } + return $out; +} + =head1 METHODS =over 4 -- cgit v1.2.1 From 32365ef65ca6a40b5262cf166543b1d84c6aa57d Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 3 Jul 2015 11:38:23 -0700 Subject: make new gross sales calculation optional, #25943 --- FS/FS/Report/Table/Monthly.pm | 8 +++++++- FS/FS/cust_bill/Search.pm | 47 +++++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 18 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table/Monthly.pm b/FS/FS/Report/Table/Monthly.pm index 0ff7efd16..f4ba02008 100644 --- a/FS/FS/Report/Table/Monthly.pm +++ b/FS/FS/Report/Table/Monthly.pm @@ -182,9 +182,15 @@ sub data { push @{$data{label}}, "$smonth/$syear"; # sprintf? my $speriod = timelocal(0,0,0,1,$smonth-1,$syear); - push @{$data{speriod}}, $speriod; if ( ++$smonth == 13 ) { $syear++; $smonth=1; } my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear); + # 12-month mode: show results in a sliding window ending at $eperiod, + # but starting 12 months before. + if ( $self->{'12mo'}) { + $speriod = timelocal(0,0,0,1,$smonth-1,$syear-1); + } + + push @{$data{speriod}}, $speriod; push @{$data{eperiod}}, $eperiod; my $col = 0; # a "column" here is the data corresponding to an item diff --git a/FS/FS/cust_bill/Search.pm b/FS/FS/cust_bill/Search.pm index 62c55d6df..38f11d165 100644 --- a/FS/FS/cust_bill/Search.pm +++ b/FS/FS/cust_bill/Search.pm @@ -7,6 +7,7 @@ use FS::Record qw( qsearchs dbh ); use FS::cust_main; use FS::access_user; use FS::Conf; +use charnames ':full'; =item search HASHREF @@ -18,7 +19,9 @@ following additional parameters valid: =over 4 -=item newest_percust +=item newest_percust - only show the most recent invoice for each customer + +=item invoiced - show the invoiced amount (excluding discounts) instead of gross sales =back @@ -27,7 +30,8 @@ following additional parameters valid: sub search { my( $class, $params ) = @_; - my( $count_query, $count_addl ) = ( '', '' ); + my $count_query = ''; + my @count_addl; #some false laziness w/cust_bill::re_X @@ -77,21 +81,30 @@ sub search { my $money = (FS::Conf->new->config('money_char') || '$') . '%.2f'; - $count_query = 'SELECT COUNT(*), '. join(', ', - map "SUM($_)", - ( 'charged + discounted', - 'discounted', - 'credited', - 'charged - credited', - 'charged - credited - paid', - ) - ); - $count_addl = [ "$money sales (gross)", - "− $money discounted", - "− $money credited", - "= $money sales (net)", + my @sums = ( 'credited', # credits + 'charged - credited', # net sales + 'charged - credited - paid', # balance due + ); + + @count_addl = ( "\N{MINUS SIGN} $money credited", + "= $money net sales", "$money outstanding balance", - ]; + ); + + if ( $params->{'invoiced'} ) { + + unshift @sums, 'charged'; + unshift @count_addl, "$money invoiced"; + + } else { + + unshift @sums, 'charged + discounted', 'discounted'; + unshift @count_addl, "$money gross sales", + "\N{MINUS SIGN} $money discounted"; + + } + + $count_query = 'SELECT COUNT(*), '. join(', ', map "SUM($_)", @sums); } $count_query .= " FROM cust_bill $join $extra_sql"; @@ -115,7 +128,7 @@ sub search { 'order_by' => 'ORDER BY '. ( $params->{'order_by'} || 'cust_bill._date' ), 'count_query' => $count_query, - 'count_addl' => $count_addl, + 'count_addl' => \@count_addl, }; } -- cgit v1.2.1 From d1f83ecbaefdab8fbb3a05e87448f3351d9051b4 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 3 Jul 2015 19:46:00 -0500 Subject: RT#34078: Payment History Report / Statement [various fixes, integrated with selfservice] --- FS/FS/ClientAPI/MasonComponent.pm | 1 + FS/FS/cust_main.pm | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm index 50597e2cb..3c3bf4cb3 100644 --- a/FS/FS/ClientAPI/MasonComponent.pm +++ b/FS/FS/ClientAPI/MasonComponent.pm @@ -14,6 +14,7 @@ $DEBUG = 0; $me = '[FS::ClientAPI::MasonComponent]'; my %allowed_comps = map { $_=>1 } qw( + /elements/customer-statement.html /elements/select-did.html /misc/areacodes.cgi /misc/exchanges.cgi diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f2c2b4ab4..46df3ffd8 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4175,7 +4175,7 @@ sub payment_history { 'amount' => sprintf('%.2f', $_->setup + $_->recur ), 'charged' => sprintf('%.2f', $_->setup + $_->recur ), 'date' => $cust_bill->_date, - 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ), + 'date_pretty' => $self->time2str_local('short', $cust_bill->_date ), } foreach $cust_bill->cust_bill_pkg; @@ -4189,7 +4189,7 @@ sub payment_history { 'amount' => sprintf('%.2f', $_->charged ), 'charged' => sprintf('%.2f', $_->charged ), 'date' => $_->_date, - 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + 'date_pretty' => $self->time2str_local('short', $_->_date ), } foreach $self->cust_bill; @@ -4201,7 +4201,7 @@ sub payment_history { 'amount' => sprintf('%.2f', 0 - $_->paid ), 'paid' => sprintf('%.2f', $_->paid ), 'date' => $_->_date, - 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + 'date_pretty' => $self->time2str_local('short', $_->_date ), } foreach $self->cust_pay; @@ -4211,7 +4211,7 @@ sub payment_history { 'amount' => sprintf('%.2f', 0 -$_->amount ), 'credit' => sprintf('%.2f', $_->amount ), 'date' => $_->_date, - 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + 'date_pretty' => $self->time2str_local('short', $_->_date ), } foreach $self->cust_credit; @@ -4221,7 +4221,7 @@ sub payment_history { 'amount' => $_->refund, 'refund' => $_->refund, 'date' => $_->_date, - 'date_pretty' => time2str('%m/%d/%Y', $_->_date ), + 'date_pretty' => $self->time2str_local('short', $_->_date ), } foreach $self->cust_refund; -- cgit v1.2.1 From 6400a0ec71fc47b217f4ab12bf95b74b77aeb260 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 6 Jul 2015 14:30:10 -0700 Subject: optionally include discounts in daily cashflow report, #37123 --- FS/FS/Report/Table/Daily.pm | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table/Daily.pm b/FS/FS/Report/Table/Daily.pm index 66739379d..8d623e766 100644 --- a/FS/FS/Report/Table/Daily.pm +++ b/FS/FS/Report/Table/Daily.pm @@ -121,16 +121,20 @@ sub data { my @newdata = (); my @newcolors = (); my @newlinks = (); + my @indices = (); foreach my $item ( @{$self->{'items'}} ) { - if ( grep { $_ != 0 } @{$data{'data'}->[$col]} ) { - push @newitems, $data{'items'}->[$col]; - push @newlabels, $data{'item_labels'}->[$col]; - push @newdata, $data{'data'}->[$col]; - push @newcolors, $data{'colors'}->[$col]; - push @newlinks, $data{'links'}->[$col]; - } - + my $is_nonzero = scalar( grep { $_ != 0 } @{ $data{'data'}->[$col] }); + next if ($self->{'remove_empty'} and $is_nonzero == 0); + # no daily reports can normalize yet + push @newitems, $data{'items'}->[$col]; + push @newlabels, $data{'item_labels'}->[$col]; + push @newdata, $data{'data'}->[$col]; + push @newcolors, $data{'colors'}->[$col]; + push @newlinks, $data{'links'}->[$col]; + push @indices, $col; + + } continue { $col++; } @@ -139,7 +143,10 @@ sub data { $data{'data'} = \@newdata; $data{'colors'} = \@newcolors; $data{'links'} = \@newlinks; + $data{'indices'} = \@indices; + } else { # not doing remove_empty; report back that all columns are included + $data{'indices'} = [ 0 .. scalar( @{$self->{'items'}} ) - 1 ]; } \%data; -- cgit v1.2.1 From 573a1f97af61acd6d31c70321acbf7bb06bbcebf Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 6 Jul 2015 21:16:33 -0500 Subject: RT#24684: Payments for Online Bill Pay [Credit Balance Display] --- FS/FS/ClientAPI/MyAccount.pm | 4 +++- FS/FS/Misc.pm | 19 ++++++++++++++++++- FS/FS/cust_main.pm | 5 ++--- 3 files changed, 23 insertions(+), 5 deletions(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 85f85def9..420ed0688 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -23,7 +23,7 @@ use FS::Conf; #use FS::UID qw(dbh); use FS::Record qw(qsearch qsearchs dbh); use FS::Msgcat qw(gettext); -use FS::Misc qw(card_types); +use FS::Misc qw(card_types money_pretty); use FS::Misc::DateTime qw(parse_datetime); use FS::TicketSystem; use FS::ClientAPI_SessionCache; @@ -609,6 +609,7 @@ sub customer_info_short { $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} ) : '(none)'; } + $return{balance_pretty} = money_pretty($return{balance}); $return{countrydefault} = scalar($conf->config('countrydefault')); @@ -692,6 +693,7 @@ sub billing_history { } $return{balance} = $cust_main->balance; + $return{balance_pretty} = money_pretty($return{balance}); $return{next_bill_date} = $cust_main->next_bill_date; $return{next_bill_date_pretty} = $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} ) diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 9aeff93a6..e1f654c34 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -23,6 +23,7 @@ use Encode; csv_from_fixed ocr_image bytes_substr + money_pretty ); $DEBUG = 0; @@ -828,7 +829,7 @@ sub _pslatex { } return if -e "$file.dvi" && -s "$file.dvi"; - die "pslatex $file.tex failed; see $file.log for details?\n"; + die "pslatex $file.tex failed, see $file.log for details?\n"; } @@ -982,6 +983,22 @@ sub bytes_substr { return Encode::decode('utf8', $bytes, $chk); } +=item money_pretty + +Accepts a postive or negative numerical value. +Returns amount formatted for display, +including money character. + +=cut + +sub money_pretty { + my $amount = shift; + my $money_char = $conf->{'money_char'} || '$'; + $amount = sprintf("%0.2f",$amount); + $amount =~ s/^(-?)/$1$money_char/; + return $amount; +} + =back =head1 BUGS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 46df3ffd8..c6602c1a0 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -32,7 +32,7 @@ use Locale::Country; use FS::UID qw( dbh driver_name ); use FS::Record qw( qsearchs qsearch dbdef regexp_sql ); use FS::Cursor; -use FS::Misc qw( generate_email send_email generate_ps do_print ); +use FS::Misc qw( generate_email send_email generate_ps do_print money_pretty ); use FS::Msgcat qw(gettext); use FS::CurrentUser; use FS::TicketSystem; @@ -4241,8 +4241,7 @@ sub payment_history { } $$item{'balance'} = sprintf("%.2f",$balance); foreach my $key ( qw(amount balance) ) { - $$item{$key.'_pretty'} = $$item{$key}; - $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/; + $$item{$key.'_pretty'} = money_pretty($$item{$key}); } push(@out,$item); } -- cgit v1.2.1 From 6de4864902314891cfabcd0adae758854114b1c7 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 6 Jul 2015 23:10:57 -0700 Subject: hide the old typeset statements with a non-default (and not added on upgrade) ACL, RT#34078 --- FS/FS/AccessRight.pm | 2 ++ 1 file changed, 2 insertions(+) (limited to 'FS') diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 1609f085f..0493edd33 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -192,6 +192,7 @@ tie my %rights, 'Tie::IxHash', 'View customer pending payments', #NEW 'Edit customer pending payments', #NEW 'View customer billing events', #NEW + 'View legacy typeset statements', #new, but no need to phase in ], ### @@ -457,6 +458,7 @@ sub default_superuser_rights { 'Echeck void', 'Void invoices',#people are overusing this when credits are more appropriate 'Backdate credit', + 'View legacy typeset statments', ); no warnings 'uninitialized'; -- cgit v1.2.1 From 4a03b0b62cef514f9217e7be61cf7a16218ea387 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 7 Jul 2015 11:31:42 -0700 Subject: optionally include discounts in sales report, #37124 --- FS/FS/Report/Table.pm | 58 ++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index eef983d68..cba968b43 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -439,8 +439,8 @@ sub cust_pkg_recur_cost { =item cust_bill_pkg: the total package charges on invoice line items. -'charges': limit the type of charges included (setup, recur, usage). -Should be a string containing one or more of 'S', 'R', or 'U'; if +'charges': limit the type of charges included (setup, recur, usage, discount). +Should be a string containing one or more of 'S', 'R', 'U', or 'D'; if unspecified, defaults to all three. 'classnum': limit to this package class. @@ -470,6 +470,7 @@ sub cust_bill_pkg { $sum += $self->cust_bill_pkg_setup(@_) if $charges{S}; $sum += $self->cust_bill_pkg_recur(@_) if $charges{R}; $sum += $self->cust_bill_pkg_detail(@_) if $charges{U}; + $sum += $self->cust_bill_pkg_discount(@_) if $charges{D}; if ($opt{'average_per_cust_pkg'}) { my $count = $self->cust_bill_pkg_count_pkgnum(@_); @@ -656,47 +657,28 @@ sub cust_bill_pkg_detail { } sub cust_bill_pkg_discount { - my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_; - - #need to do this the new multi-classnum way if it gets re-enabled - #my $where = ''; - #my $comparison = ''; - #if ( $opt{'classnum'} =~ /^(\d+)$/ ) { - # if ( $1 == 0 ) { - # $comparison = "IS NULL"; - # } else { - # $comparison = "= $1"; - # } - # - # if ( $opt{'use_override'} ) { - # $where = "( - # part_pkg.classnum $comparison AND pkgpart_override IS NULL OR - # override.classnum $comparison AND pkgpart_override IS NOT NULL - # )"; - # } else { - # $where = "part_pkg.classnum $comparison"; - # } - #} + my $self = shift; + my ($speriod, $eperiod, $agentnum, %opt) = @_; + # apply all the same constraints here as for setup/recur $agentnum ||= $opt{'agentnum'}; - my $total_sql = - " SELECT COALESCE( SUM( cust_bill_pkg_discount.amount ), 0 ) "; + my @where = ( + '(pkgnum != 0 OR feepart IS NOT NULL)', + $self->with_classnum($opt{'classnum'}, $opt{'use_override'}), + $self->with_report_option(%opt), + $self->in_time_period_and_agent($speriod, $eperiod, $agentnum), + $self->with_refnum(%opt), + $self->with_cust_classnum(%opt) + ); - $total_sql .= - " FROM cust_bill_pkg_discount - LEFT JOIN cust_bill_pkg USING ( billpkgnum ) - LEFT JOIN cust_bill USING ( invnum ) - LEFT JOIN cust_main USING ( custnum ) - WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum); - # LEFT JOIN cust_pkg_discount USING ( pkgdiscountnum ) - # LEFT JOIN discount USING ( discountnum ) - # LEFT JOIN cust_pkg USING ( pkgnum ) - # LEFT JOIN part_pkg USING ( pkgpart ) - # LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart - - return $self->scalar_sql($total_sql); + my $total_sql = "SELECT COALESCE(SUM(cust_bill_pkg_discount.amount), 0) + FROM cust_bill_pkg_discount + JOIN cust_bill_pkg USING (billpkgnum) + $cust_bill_pkg_join + WHERE " . join(' AND ', grep $_, @where); + $self->scalar_sql($total_sql); } ##### package churn report ##### -- cgit v1.2.1 From 04969741a587fa292c830b83b077c4c7522621b9 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 8 Jul 2015 17:38:36 -0500 Subject: RT#36889: cust-fields options to include agent --- FS/FS/ConfDefaults.pm | 3 +++ FS/FS/UI/Web.pm | 1 + 2 files changed, 4 insertions(+) (limited to 'FS') diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index de08f7d68..5b8399af5 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -71,6 +71,9 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance', + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type | Current Balance | Agent Cust#' => + 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance | Agent Cust#', + 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Payment Type | Current Balance', diff --git a/FS/FS/UI/Web.pm b/FS/FS/UI/Web.pm index ca50b278e..6655f270f 100644 --- a/FS/FS/UI/Web.pm +++ b/FS/FS/UI/Web.pm @@ -273,6 +273,7 @@ sub cust_header { 'Invoicing email(s)' => 'invoicing_list_emailonly_scalar', 'Payment Type' => 'payby', 'Current Balance' => 'current_balance', + 'Agent Cust#' => 'agent_custid', ); $header2method{'Cust#'} = 'display_custnum' if $conf->exists('cust_main-default_agent_custid'); -- cgit v1.2.1 From 4f5619288413a185e9933088d9dd8c5afbc55dfa Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 8 Jul 2015 19:15:33 -0500 Subject: RT#34078: Payment History Report / Statement [Fixes for acl and text preview] --- FS/FS/cust_main.pm | 2 ++ 1 file changed, 2 insertions(+) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index c6602c1a0..f55c3493c 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4253,6 +4253,8 @@ sub payment_history { 'description' => 'Previous balance', 'amount' => sprintf("%.2f",$previous), 'balance' => sprintf("%.2f",$previous), + 'date' => $$opt{'start_date'}, + 'date_pretty' => $self->time2str_local('short', $$opt{'start_date'} ), }; #false laziness with above foreach my $key ( qw(amount balance) ) { -- cgit v1.2.1 From 917b645ae876153480207c9118acd6099531955f Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Thu, 9 Jul 2015 23:23:48 -0700 Subject: update our Mason.pm for RT 4.2, RT#13852 --- FS/FS/Mason.pm | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index f05ac0339..297d591ea 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -433,21 +433,13 @@ if ( -e $addl_handler_use_file ) { use RT::CustomFieldValues; use RT::ObjectCustomFieldValues; - #blah. manually updated from RT::Interface::Web::Handler - use RT::Interface::Web; - use MIME::Entity; - use Text::Wrapper; - use Time::ParseDate; - use Time::HiRes; - use HTML::Scrubber; + use RT::Interface::Web::Handler; #blah. not even in RT::Interface::Web::Handler, just in #html/NoAuth/css/dhandler and rt-test-dependencies. ask for it here #to throw a real error instead of just a mysterious unstyled RT use CSS::Squish 0.06; - use RT::Interface::Web::Request; - #another undeclared web UI dep (for ticket links graph) use IPC::Run::SafeHandles; -- cgit v1.2.1 From 32eacfe9022ab9edb6fd986618ac2d3949fc7dcd Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 10 Jul 2015 14:29:33 -0700 Subject: avoid sending SureTax requests when no items are taxable --- FS/FS/TaxEngine/suretax.pm | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'FS') diff --git a/FS/FS/TaxEngine/suretax.pm b/FS/FS/TaxEngine/suretax.pm index 8139b1dff..4e7edd575 100644 --- a/FS/FS/TaxEngine/suretax.pm +++ b/FS/FS/TaxEngine/suretax.pm @@ -85,6 +85,8 @@ sub build_request { my @lines = map { $self->build_item($_) } $cust_bill->cust_bill_pkg; + return if !@lines; + my $ClientNumber = $conf->config('suretax-client_number') or die "suretax-client_number config required.\n"; my $ValidationKey = $conf->config('suretax-validation_key') @@ -306,6 +308,10 @@ sub make_taxlines { # assemble the request hash my $request = $self->build_request; + if (!$request) { + warn "no taxable items in invoice; skipping SureTax request\n" if $DEBUG; + return; + } warn "sending SureTax request\n" if $DEBUG; my $request_json = $json->encode($request); -- cgit v1.2.1 From 5b193ee2c5fae0743699adb1a28591f2a1735eb7 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 10 Jul 2015 18:14:40 -0700 Subject: avoid generating spurious zero-amount bills on cancellation, #37229, from #16066 --- FS/FS/cust_main/Billing.pm | 8 ++++++++ FS/FS/part_pkg/flat.pm | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index f4c804568..87be4e68d 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1107,6 +1107,14 @@ sub _make_lines { return "$@ running $method for $cust_pkg\n" if ( $@ ); + if ($recur eq 'NOTHING') { + # then calc_cancel (or calc_recur but that's not used) has declined to + # generate a recurring lineitem at all. treat this as zero, but also + # try not to generate a lineitem. + $recur = 0; + $lineitems--; + } + #base_cancel??? $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 930966a94..eb70253bb 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -220,13 +220,13 @@ sub calc_cancel { and $self->option('bill_recur_on_cancel', 1) ) { # run another recurring cycle return $self->calc_recur(@_); - } - elsif ( $conf->exists('bill_usage_on_cancel') # should be a package option? + } elsif ( $conf->exists('bill_usage_on_cancel') # should be a package option? and $self->can('calc_usage') ) { # bill for outstanding usage return $self->calc_usage(@_); + } else { + return 'NOTHING'; # numerically zero, but has special meaning } - 0; } sub calc_remain { -- cgit v1.2.1 From 88dbc5b1692d37bac7cd825ef4b61cf971c40677 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 10 Jul 2015 18:38:51 -0700 Subject: elimiate warnings about RT method changing from EscapeUTF8 to EscapeHTML, RT#13852 --- FS/FS/Mason.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 297d591ea..ff57b4353 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -646,7 +646,8 @@ sub mason_interps { [ 'rt' => '%%%FREESIDE_DOCUMENT_ROOT%%%/rt' ], [ 'freeside' => '%%%FREESIDE_DOCUMENT_ROOT%%%' ], ], - escape_flags => { 'h' => \&RT::Interface::Web::EscapeUTF8, + escape_flags => { 'h' => \&RT::Interface::Web::EscapeHTML, + #u and j aren't used anymore? :/ 'u' => \&RT::Interface::Web::EscapeURI, 'j' => \&RT::Interface::Web::EscapeJS, 'js_string' => $js_string_sub, -- cgit v1.2.1 From 5376c66e2e5c1849763777f9d44ad013405c1d9e Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 10 Jul 2015 21:42:43 -0700 Subject: include agent and restore cust-level status in small_custview --- FS/FS/UI/Web/small_custview.pm | 6 +++++- FS/FS/access_user.pm | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm index a1173f7da..e82e3326f 100644 --- a/FS/FS/UI/Web/small_custview.pm +++ b/FS/FS/UI/Web/small_custview.pm @@ -95,8 +95,12 @@ sub small_custview { $html = qq!' if $url; + if ( $FS::CurrentUser::CurrentUser->num_agents ) { + $html .= encode_entities($cust_main->agent->agent). ' '; + } + $html .= 'Customer #'. $cust_main->display_custnum. - ': '. encode_entities($cust_main->name). ''; + ': '. encode_entities($cust_main->name). ''. ' - '. $cust_main->status_label. ''; diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index 68d2deaba..a3f55bc76 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -283,6 +283,22 @@ sub report_sales { Returns links to the the groups this user is a part of, as FS::access_usergroup objects (see L). +=item num_agents + +Returns the number of agents this user can view (via group membership). + +=cut + +sub num_agents { + my $self = shift; + $self->scalar_sql( + 'SELECT COUNT(DISTINCT agentnum) FROM access_usergroup + JOIN access_groupagent USING ( groupnum ) + WHERE usernum = ?', + $self->usernum, + ); +} + =item agentnums Returns a list of agentnums this user can view (via group membership). -- cgit v1.2.1 From 823d03145596a92a347a0682c64ab1e00818696b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 10 Jul 2015 23:57:35 -0700 Subject: include is deprecated --- FS/FS/Mason.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index ff57b4353..464e092dc 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -509,7 +509,7 @@ if ( -e $addl_handler_use_file ) { sub include { use vars qw($m); - #carp #should just switch to <& &> syntax + carp 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp)'; $m->scomp(@_); } -- cgit v1.2.1 From 0736a91a87c46f7097ddef9782ddbb59a0ce3304 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 11 Jul 2015 14:40:31 -0700 Subject: include is deprecated --- FS/FS/Mason.pm | 1 + 1 file changed, 1 insertion(+) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 464e092dc..d0ac8a6ca 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -509,6 +509,7 @@ if ( -e $addl_handler_use_file ) { sub include { use vars qw($m); + use Carp; carp 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp)'; $m->scomp(@_); } -- cgit v1.2.1 From 7e67c654c102bb5d9a62229161f7e8464f36a71a Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 11 Jul 2015 23:32:26 -0700 Subject: include is deprecated - improve deprecation message w/filename --- FS/FS/Mason.pm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index d0ac8a6ca..d1535b5a3 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -508,9 +508,8 @@ if ( -e $addl_handler_use_file ) { }; sub include { - use vars qw($m); - use Carp; - carp 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp)'; + use vars qw($m @CARP_NOT); + warn 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp) at '. $m->callers(1)->path. "\n"; $m->scomp(@_); } -- cgit v1.2.1 From 990439e2c8c545ea75ba5ded346fd51c4560b805 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Sat, 11 Jul 2015 23:39:16 -0700 Subject: include is deprecated, but not ready to enable this warning after all --- FS/FS/Mason.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index d1535b5a3..3d577f6d2 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -508,8 +508,8 @@ if ( -e $addl_handler_use_file ) { }; sub include { - use vars qw($m @CARP_NOT); - warn 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp) at '. $m->callers(1)->path. "\n"; + use vars qw($m); + #warn 'include deprecated; use an HTML::Mason <& &> style include (or $m->scomp) at '. $m->callers(0)->path. "\n"; $m->scomp(@_); } -- cgit v1.2.1 From 8f1f857ffd2f01a543bdef6b8486ad8c90bd302a Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 13 Jul 2015 13:17:10 -0700 Subject: batchconfig-nacha-origin_name --- FS/FS/Conf.pm | 7 +++++++ FS/FS/pay_batch/nacha.pm | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index b384d85af..c31414914 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3974,6 +3974,13 @@ and customer address. Include units.', 'type' => 'text', }, + { + 'key' => 'batchconfig-nacha-origin_name', + 'section' => 'billing', + 'description' => 'Configuration for NACHA batching, Origin name (defaults to company name, but sometimes bank name is needed instead.)'. + 'type' => 'text', + }, + { 'key' => 'batch-manual_approval', 'section' => 'billing', diff --git a/FS/FS/pay_batch/nacha.pm b/FS/FS/pay_batch/nacha.pm index 0361304e3..befba09a2 100644 --- a/FS/FS/pay_batch/nacha.pm +++ b/FS/FS/pay_batch/nacha.pm @@ -47,7 +47,12 @@ $DEBUG = 0; my $origin = $1; my $company = $conf->config('company_name', $pay_batch->agentnum); - $company = substr(uc($company). (' 'x23), 0, 23); + + my $origin_name = $conf->config('batchconfig-nacha-origin_name') + || $company; + $origin_name = substr(uc($origin_name). (' 'x23), 0, 23); + + $company = substr(uc($company). (' 'x16), 0, 16); my $now = time; @@ -78,7 +83,7 @@ $DEBUG = 0; '10'. #Blocking Factor '1'. #Format code $dest_name. #Immediate Destination Name / 23 char bank name - $company. #Immediate Origin Name / 23 char company name + $origin_name. #Immediate Origin Name / 23 char company name $refcode. "\n". #Reference Code (internal/optional) ### @@ -88,7 +93,7 @@ $DEBUG = 0; '5'. #Record Type Code '225'. #Service Class Code (220 credits only, # 200 mixed debits & credits) - substr($company, 0, 16). #on cust. statements + $company. #on cust. statements (' 'x20 ). #20 char "company internal use if desired" $origin. #Company Identification (Immediate Origin) 'PPD'. #others? -- cgit v1.2.1 From 2ead33f915536127cd148d4e2ca474b9c45c634b Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 13 Jul 2015 15:45:15 -0700 Subject: fix tyop in batchconfig-nacha-origin_name --- FS/FS/Conf.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index c31414914..1d4a85a57 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3977,7 +3977,7 @@ and customer address. Include units.', { 'key' => 'batchconfig-nacha-origin_name', 'section' => 'billing', - 'description' => 'Configuration for NACHA batching, Origin name (defaults to company name, but sometimes bank name is needed instead.)'. + 'description' => 'Configuration for NACHA batching, Origin name (defaults to company name, but sometimes bank name is needed instead.)', 'type' => 'text', }, -- cgit v1.2.1 From 4b695753d2456060e6a16808120cbb488a19c584 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 13 Jul 2015 18:33:52 -0500 Subject: RT#31594: Unapplied payment issues --- FS/FS/Schema.pm | 1 + FS/FS/cust_bill.pm | 5 ++++- FS/FS/cust_main/Billing.pm | 4 +++- FS/FS/cust_pay.pm | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 24ca85821..eb5f1d3b2 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -2431,6 +2431,7 @@ sub tables_hashref { 'payunique', 'varchar', 'NULL', $char_d, '', '',#separate paybatch "unique" functions from current usage 'closed', 'char', 'NULL', 1, '', '', 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances + 'no_auto_apply', 'char', 'NULL', 1, '', '', # cash/check deposit info fields 'bank', 'varchar', 'NULL', $char_d, '', '', diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index db909309f..5052ed130 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -888,6 +888,7 @@ sub hide { =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits to this invoice. +Payments with the no_auto_apply flag set will not be applied. A hash of optional arguments may be passed. Currently "manual" is supported. If true, a payment receipt is sent instead of a statement when @@ -914,7 +915,9 @@ sub apply_payments_and_credits { $self->select_for_update; #mutex - my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; + my @payments = grep { $_->unapplied > 0 } + grep { !$_->no_auto_apply } + $self->cust_main->cust_pay; my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; if ( $conf->exists('pkg-balances') ) { diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 87be4e68d..df7e17f81 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -2177,6 +2177,7 @@ sub due_cust_event { =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits. +Payments with the no_auto_apply flag set will not be applied. In most cases, this new method should be used in place of sequential apply_payments and apply_credits methods. @@ -2319,6 +2320,7 @@ sub apply_credits { Applies (see L) unapplied payments (see L) to outstanding invoice balances in chronological order. +Payments with the no_auto_apply flag set will not be applied. #and returns the value of any remaining unapplied payments. @@ -2348,7 +2350,7 @@ sub apply_payments { #return 0 unless - my @payments = $self->unapplied_cust_pay; + my @payments = grep { !$_->no_auto_apply } $self->unapplied_cust_pay; my @invoices = $self->open_cust_bill; diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 8b4c98ad6..d135599a3 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -116,6 +116,10 @@ books closed flag, empty or `Y' Desired pkgnum when using experimental package balances. +=item no_auto_apply + +Flag to only allow manual application of payment, empty or 'Y' + =item bank The bank where the payment was deposited. @@ -539,6 +543,7 @@ sub check { || $self->ut_textn('paybatch') || $self->ut_textn('payunique') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_flag('no_auto_apply') || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') || $self->ut_textn('bank') || $self->ut_alphan('depositor') -- cgit v1.2.1 From 204aaf4c7b3ba6a2416a4133ba0079305b4f4bdc Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 13 Jul 2015 18:16:00 -0700 Subject: move agent_custid next to custnum, #36889 --- FS/FS/ConfDefaults.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm index 5b8399af5..b24a300f9 100644 --- a/FS/FS/ConfDefaults.pm +++ b/FS/FS/ConfDefaults.pm @@ -71,8 +71,8 @@ sub cust_fields_avail { ( 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance', - 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type | Current Balance | Agent Cust#' => - 'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance | Agent Cust#', + 'Cust# | Agent Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Payment Type | Current Balance' => + 'custnum | Agent Cust# | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Payment Type | Current Balance', 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Payment Type | Current Balance' => 'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Payment Type | Current Balance', -- cgit v1.2.1 From 7c1e0242d6e2d8d45001bbb3b1b828fb425953c1 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 13 Jul 2015 22:38:38 -0700 Subject: fix autogeneration of scrips and related objects under RT 4.2, #18184, #13852 --- FS/FS/TicketSystem.pm | 92 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 36 deletions(-) (limited to 'FS') diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index e81d89328..8f3d7af03 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -159,22 +159,29 @@ sub _upgrade_data { my $search = $class->new($CurrentUser); $search->UnLimit; while ( my $item = $search->Next ) { - my $ids = $hash->{lc($item->Name)} ||= []; - if ( $item->Creator == 1 ) { # RT::SystemUser - unshift @$ids, $item->Id; - } - else { - push @$ids, $item->Id; + if ( $class =~ /Template/ ) { + # template names can be duplicated in different queues, and they are. + my $queue = $item->QueueObj->Name || '0'; + my $subhash = $hash->{$queue} ||= {}; + $subhash->{lc($item->Name)} = $item->Id; + } else { + # then duplicate names are allowed; they just have different ids + my $ids = $hash->{lc($item->Name)} ||= []; + if ( $item->Creator == 1 ) { # RT::SystemUser + unshift @$ids, $item->Id; + } + else { + push @$ids, $item->Id; + } } } }; my (%condition, %action, %template); - &$cachify('RT::ScripConditions', \%condition); - &$cachify('RT::ScripActions', \%action); - &$cachify('RT::Templates', \%template); - # $condition{name} = [ ids... ] + &$cachify('RT::ScripConditions', \%condition); # condition name -> [ ids ] # with the id of the system-created object first, if there is one + &$cachify('RT::ScripActions', \%action); # action name -> [ ids ] + &$cachify('RT::Templates', \%template); # queue name -> tmpl name -> id # ScripConditions my $ScripCondition = RT::ScripCondition->new($CurrentUser); @@ -196,40 +203,50 @@ sub _upgrade_data { $action{ lc($ScripAction->Name) } = [ $ScripAction->Id ]; } + $DB::single = 1; # Templates my $Template = RT::Template->new($CurrentUser); foreach my $t (@Templates) { # $t: Queue, Name, Description, Content - next if exists( $template{ lc($t->{Name}) } ); + next if exists( $template{ $t->{Queue} }->{ lc($t->{Name}) } ); my ($val, $msg) = $Template->Create( %$t ); die $msg if !$val; - $template{ lc($Template->Name) } = [ $Template->Id ]; + $template{ $t->{Queue} }->{ lc($Template->Name) } = [ $Template->Id ]; } # Scrips + # Scrips can no longer be deleted, so we'll count them as existing + # if they're applied to the global queue, or if they're not applied to + # _any_ queue. + my %scrip; # $scrips{condition}{action}{template} = id - my $search = RT::Scrips->new($CurrentUser); - $search->Limit(FIELD => 'Queue', VALUE => 0); - while (my $item = $search->Next) { - my ($c, $a, $t) = map {lc $item->$_->Name} - ('ScripConditionObj', 'ScripActionObj', 'TemplateObj'); - if ( exists $scrip{$c}{$a} and $item->Creator == 1 ) { - warn "Deleting duplicate scrip $c $a [$t]\n"; - my ($val, $msg) = $item->Delete; - warn "error deleting scrip: $msg\n" if !$val; - } - elsif ( exists $Delete_Scrips{$c}{$a}{$t} and $item->Creator == 1 ) { - warn "Deleting obsolete scrip $c $a [$t]\n"; - my ($val, $msg) = $item->Delete; - warn "error deleting scrip: $msg\n" if !$val; - } - else { - $scrip{$c}{$a} = $item->id; + foreach my $criterion ('LimitToGlobal', 'LimitToNotAdded') { + my $search = RT::Scrips->new($CurrentUser); + $search->$criterion; + + while (my $item = $search->Next) { + my ($c, $a, $t) = map {lc $item->$_->Name} + ('ScripConditionObj', 'ScripActionObj', 'TemplateObj'); + if ( exists $scrip{$c}{$a} and $item->Creator == 1 ) { + warn "Deleting duplicate scrip $c $a [$t]\n"; + my ($val, $msg) = $item->Delete; + warn "error deleting scrip: $msg\n" if !$val; + } + elsif ( exists $Delete_Scrips{$c}{$a}{$t} and $item->Creator == 1 ) { + warn "Deleting obsolete scrip $c $a [$t]\n"; + my ($val, $msg) = $item->Delete; + warn "error deleting scrip: $msg\n" if !$val; + } + else { + $scrip{$c}{$a} = $item->id; + } } } + my $Scrip = RT::Scrip->new($CurrentUser); foreach my $s ( @Scrips ) { my $desc = $s->{'Description'}; + # the condition, action, and template _names_ my ($c, $a, $t) = map lc, @{ $s }{'ScripCondition', 'ScripAction', 'Template'}; @@ -245,14 +262,15 @@ sub _upgrade_data { warn "ScripAction '$a' not found.\n"; next; } - if ( !exists($template{$t}) ) { + if ( !exists($template{'0'}{$t}) ) { + # a global template with this name has to exist, at least warn "Template '$t' not found.\n"; next; } my %new_param = ( ScripCondition => $condition{$c}->[0], ScripAction => $action{$a}->[0], - Template => $template{$t}->[0], + Template => $t, # scrips.template is now the name, not the id Queue => 0, Description => $desc, ); @@ -262,11 +280,13 @@ sub _upgrade_data { } #if $scrip{...} # set the Immutable attribute on them if needed - if ( !$Scrip->FirstAttribute('Immutable') ) { - my ($val, $msg) = - $Scrip->SetAttribute(Name => 'Immutable', Content => '1'); - die $msg if !$val; - } + # no longer needed; you can't delete scrips through the UI anyway, only + # disable them + #if ( !$Scrip->FirstAttribute('Immutable') ) { + # my ($val, $msg) = + # $Scrip->SetAttribute(Name => 'Immutable', Content => '1'); + # die $msg if !$val; + #} } #foreach (@Scrips) -- cgit v1.2.1 From 57e3a0e08b81d52851314c60f37115a05b9be79e Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 14 Jul 2015 00:00:07 -0700 Subject: fix invoice report when there are no customer classes, #37243, from #25943 --- FS/FS/Report/Table.pm | 1 + FS/FS/cust_bill/Search.pm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index cba968b43..0a0d24a79 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -901,6 +901,7 @@ sub with_classnum { $classnum = [ $classnum ] if !ref($classnum); @$classnum = grep /^\d+$/, @$classnum; + return '' if !@$classnum; my $in = 'IN ('. join(',', @$classnum). ')'; if ( $use_override ) { diff --git a/FS/FS/cust_bill/Search.pm b/FS/FS/cust_bill/Search.pm index 38f11d165..925938d82 100644 --- a/FS/FS/cust_bill/Search.pm +++ b/FS/FS/cust_bill/Search.pm @@ -232,7 +232,7 @@ sub search_sql_where { ? @{ $param->{'cust_classnum'} } : ( $param->{'cust_classnum'} ); - @classnum = grep /^(\d*)$/, @classnum; + @classnum = grep /^(\d+)$/, @classnum; if ( @classnum ) { push @search, 'COALESCE(cust_main.classnum, 0) IN ('.join(',', @classnum).')'; -- cgit v1.2.1 From 98f6d91ec7eaa907204afbfeb90ede1e3bff656d Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Mon, 13 Jul 2015 17:26:48 -0700 Subject: automatic package changes for supplemental packages, #37102 --- FS/FS/Schema.pm | 8 +++ FS/FS/cust_pkg.pm | 139 ++++++++++++++++++++++++++++++++++++++++--------- FS/FS/part_pkg.pm | 43 +++++++++++++++ FS/FS/part_pkg/flat.pm | 12 ----- 4 files changed, 164 insertions(+), 38 deletions(-) (limited to 'FS') diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index eb5f1d3b2..c8b9b631d 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3198,6 +3198,10 @@ sub tables_hashref { 'delay_start', 'int', 'NULL', '', '', '', 'start_on_hold', 'char', 'NULL', 1, '', '', 'agent_pkgpartid', 'varchar', 'NULL', 20, '', '', + 'expire_months', 'int', 'NULL', '', '', '', + 'adjourn_months', 'int', 'NULL', '', '', '', + 'contract_end_months','int','NULL', '', '', '', + 'change_to_pkgpart', 'int', 'NULL', '', '', '', ], 'primary_key' => 'pkgpart', 'unique' => [], @@ -3226,6 +3230,10 @@ sub tables_hashref { table => 'part_pkg', references => [ 'pkgpart' ], }, + { columns => [ 'change_to_pkgpart' ], + table => 'part_pkg', + references => [ 'pkgpart' ], + }, ], }, diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 950d348ce..fbecd8d69 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -251,19 +251,53 @@ or contract_end timers to some number of months after the start date a delayed setup fee after a period of "free days", will also set the start date to the end of that period. +If the package has an automatic transfer rule (C), then +this will also order the package and set its start date. + =cut sub set_initial_timers { my $self = shift; my $part_pkg = $self->part_pkg; + my $start = $self->start_date || $self->setup || time; + foreach my $action ( qw(expire adjourn contract_end) ) { - my $months = $part_pkg->option("${action}_months",1); + my $months = $part_pkg->get("${action}_months"); if($months and !$self->get($action)) { - my $start = $self->start_date || $self->setup || time; $self->set($action, $part_pkg->add_freq($start, $months) ); } } + # if this package has an expire date and a change_to_pkgpart, set automatic + # package transfer + # (but don't call change_later, as that would call $self->replace, and we're + # probably in the middle of $self->insert right now) + if ( $part_pkg->expire_months and $part_pkg->change_to_pkgpart ) { + if ( $self->change_to_pkgnum ) { + # this can happen if a package is ordered on hold, scheduled for a + # future change _while on hold_, and then released from hold, causing + # the automatic transfer to schedule. + # + # what's correct behavior in that case? I think it's to disallow + # future-changing an on-hold package that has an automatic transfer. + # but if we DO get into this situation, let the manual package change + # win. + warn "pkgnum ".$self->pkgnum.": manual future package change blocks ". + "automatic transfer.\n"; + } else { + my $change_to = FS::cust_pkg->new( { + start_date => $self->get('expire'), + pkgpart => $part_pkg->change_to_pkgpart, + map { $_ => $self->get($_) } + qw( custnum locationnum quantity refnum salesnum contract_end ) + } ); + my $error = $change_to->insert; + + return $error if $error; + $self->set('change_to_pkgnum', $change_to->pkgnum); + } + } + # if this package has "free days" and delayed setup fee, then # set start date that many days in the future. # (this should have been set in the UI, but enforce it here) @@ -273,6 +307,7 @@ sub set_initial_timers { { $self->start_date( $part_pkg->default_start_date ); } + ''; } @@ -332,9 +367,12 @@ a location change). sub insert { my( $self, %options ) = @_; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + my $error; $error = $self->check_pkgpart unless $options{'allow_pkgpart'}; - return $error if $error; my $part_pkg = $self->part_pkg; @@ -359,15 +397,12 @@ sub insert { $self->set('start_date', ''); } else { # set expire/adjourn/contract_end timers, and free days, if appropriate - $self->set_initial_timers; + # and automatic package transfer, which can fail, so capture the result + $error = $self->set_initial_timers; } } # else this is a package change, and shouldn't have "new package" behavior - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ()); + $error ||= $self->SUPER::insert($options{options} ? %{$options{options}} : ()); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -461,9 +496,26 @@ hide cancelled packages. =cut +# this is still used internally to abort future package changes, so it +# does need to work + sub delete { my $self = shift; + # The following foreign keys to cust_pkg are not cleaned up here, and will + # cause package deletion to fail: + # + # cust_credit.pkgnum and commission_pkgnum (and cust_credit_void) + # cust_credit_bill.pkgnum + # cust_pay_pending.pkgnum + # cust_pay.pkgnum (and cust_pay_void) + # cust_bill_pay.pkgnum (wtf, shouldn't reference pkgnum) + # cust_pkg_usage.pkgnum + # cust_pkg.uncancel_pkgnum, change_pkgnum, main_pkgnum, and change_to_pkgnum + + # cust_svc is handled by canceling the package before deleting it + # cust_pkg_option is handled via option_Common + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; @@ -499,7 +551,13 @@ sub delete { } } - #pkg_referral? + foreach my $pkg_referral ( $self->pkg_referral ) { + my $error = $pkg_referral->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } my $error = $self->SUPER::delete(@_); if ( $error ) { @@ -807,12 +865,15 @@ sub cancel { my( $self, %options ) = @_; my $error; - # pass all suspend/cancel actions to the main package - # (unless the pkglinknum has been removed, then the link is defunct and - # this package can be canceled on its own) - if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) { - return $self->main_pkg->cancel(%options); - } + # supplemental packages can now be separately canceled, though the UI + # shouldn't permit it + # + ## pass all suspend/cancel actions to the main package + ## (unless the pkglinknum has been removed, then the link is defunct and + ## this package can be canceled on its own) + #if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) { + # return $self->main_pkg->cancel(%options); + #} my $conf = new FS::Conf; @@ -936,8 +997,14 @@ sub cancel { $hash{main_pkgnum} = ''; } + # if there is a future package change scheduled, unlink from it (like + # abort_change) first, then delete it. + $hash{'change_to_pkgnum'} = ''; + + # save the package state my $new = new FS::cust_pkg ( \%hash ); $error = $new->replace( $self, options => { $self->options } ); + if ( $self->change_to_pkgnum ) { my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum); $error ||= $change_to->cancel('no_delay_cancel' => 1) || $change_to->delete; @@ -1285,9 +1352,13 @@ sub suspend { my( $self, %options ) = @_; my $error; - # pass all suspend/cancel actions to the main package + # supplemental packages still can't be separately suspended, but silently + # exit instead of failing or passing the action to the main package (so + # that the "Suspend customer" action doesn't trip over the supplemental + # packages and die) + if ( $self->main_pkgnum and !$options{'from_main'} ) { - return $self->main_pkg->suspend(%options); + return; } my $oldAutoCommit = $FS::UID::AutoCommit; @@ -1659,7 +1730,11 @@ sub unsuspend { if (!$self->setup) { # then this package is being released from on-hold status - $self->set_initial_timers; + $error = $self->set_initial_timers; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } my @labels = (); @@ -2034,12 +2109,12 @@ sub change { # almost. if the new pkgpart specifies start/adjourn/expire timers, # apply those. if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { - $self->set_initial_timers; + $error ||= $self->set_initial_timers; } # but if contract_end was explicitly specified, that overrides all else $self->set('contract_end', $opt->{'contract_end'}) if $opt->{'contract_end'}; - $error = $self->replace; + $error ||= $self->replace; if ( $error ) { $dbh->rollback if $oldAutoCommit; return "modifying package: $error"; @@ -2509,16 +2584,28 @@ Cancels a future package change scheduled by C. sub abort_change { my $self = shift; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $pkgnum = $self->change_to_pkgnum; my $change_to = FS::cust_pkg->by_key($pkgnum) if $pkgnum; my $error; - if ( $change_to ) { - $error = $change_to->cancel || $change_to->delete; - return $error if $error; - } $self->set('change_to_pkgnum', ''); $self->set('expire', ''); - $self->replace; + $error = $self->replace; + if ( $change_to ) { + $error ||= $change_to->cancel || $change_to->delete; + } + + if ( $oldAutoCommit ) { + if ( $error ) { + dbh->rollback; + } else { + dbh->commit; + } + } + + return $error; } =item set_quantity QUANTITY diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 0e9ee05fb..498da8a2b 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -127,6 +127,18 @@ part_pkg, will be equal to pkgpart. ordered. The package will not start billing or have a setup fee charged until it is manually unsuspended. +=item change_to_pkgpart - When this package is ordered, schedule a future +package change. The 'expire_months' field will determine when the package +change occurs. + +=item expire_months - Number of months until this package expires (or changes +to another package). + +=item adjourn_months - Number of months until this package becomes suspended. + +=item contract_end_months - Number of months until the package's contract +ends. + =back =head1 METHODS @@ -722,6 +734,11 @@ sub check { || $self->ut_numbern('delay_start') || $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart') || $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart') + || $self->ut_numbern('expire_months') + || $self->ut_numbern('adjourn_months') + || $self->ut_numbern('contract_end_months') + || $self->ut_numbern('change_to_pkgpart') + || $self->ut_foreign_keyn('change_to_pkgpart', 'part_pkg', 'pkgpart') || $self->ut_alphan('agent_pkgpartid') || $self->SUPER::check ; @@ -1696,6 +1713,19 @@ for this package. Returns the voice usage pools (see L) defined for this package. +=item change_to_pkg + +Returns the automatic transfer target for this package, or an empty string +if there isn't one. + +=cut + +sub change_to_pkg { + my $self = shift; + my $pkgpart = $self->change_to_pkgpart or return ''; + FS::part_pkg->by_key($pkgpart); +} + =item _rebless Reblesses the object into the FS::part_pkg::PLAN class (if available), where @@ -2202,6 +2232,19 @@ sub queueable_upgrade { FS::upgrade_journal->set_done($upgrade); } + # migrate adjourn_months, expire_months, and contract_end_months to + # real fields + foreach my $field (qw(adjourn_months expire_months contract_end_months)) { + foreach my $option (qsearch('part_pkg_option', { optionname => $field })) { + my $part_pkg = $option->part_pkg; + my $error = $option->delete; + if ( $option->optionvalue and $part_pkg->get($field) eq '' ) { + $part_pkg->set($field, $option->optionvalue); + $error ||= $part_pkg->replace; + } + die $error if $error; + } + } } =item curuser_pkgs_sql diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index eb70253bb..d11b99b1a 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -34,16 +34,6 @@ tie my %contract_years, 'Tie::IxHash', ( 'select_options' => \%temporalities, }, - #used in cust_pkg.pm so could add to any price plan - 'expire_months' => { 'name' => 'Auto-add an expiration date this number of months out', - }, - 'adjourn_months'=> { 'name' => 'Auto-add a suspension date this number of months out', - }, - 'contract_end_months'=> { - 'name' => 'Auto-add a contract end date this number of years out', - 'type' => 'select', - 'select_options' => \%contract_years, - }, #used in cust_pkg.pm so could add to any price plan where it made sense 'start_1st' => { 'name' => 'Auto-add a start date to the 1st, ignoring the current month.', 'type' => 'checkbox', @@ -85,8 +75,6 @@ tie my %contract_years, 'Tie::IxHash', ( }, }, 'fieldorder' => [ qw( recur_temporality - expire_months adjourn_months - contract_end_months start_1st sync_bill_date prorate_defer_bill prorate_round_day suspend_bill unsuspend_adjust_bill -- cgit v1.2.1 From eb265def46e7010331161ad303de715c206eca16 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 14 Jul 2015 13:18:49 -0700 Subject: allow non-integer ratios of supplemental package period, #37102 --- FS/FS/cust_main/Billing.pm | 34 +++++++++++++++++++++++++++------- FS/FS/part_pkg_link.pm | 10 ++++------ 2 files changed, 31 insertions(+), 13 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index df7e17f81..0bc0fbd39 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1133,19 +1133,39 @@ sub _make_lines { # its frequency my $main_pkg_freq = $main_pkg->part_pkg->freq; my $supp_pkg_freq = $part_pkg->freq; - my $ratio = $supp_pkg_freq / $main_pkg_freq; - if ( $ratio != int($ratio) ) { + if ( $supp_pkg_freq == 0 or $main_pkg_freq == 0 ) { # the UI should prevent setting up packages like this, but just # in case - return "supplemental package period is not an integer multiple of main package period"; + return "unable to calculate supplemental package period ratio"; } - $next_bill = $sdate; - for (1..$ratio) { - $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq ); + my $ratio = $supp_pkg_freq / $main_pkg_freq; + if ( $ratio == int($ratio) ) { + # simple case: main package is X months, supp package is X*A months, + # advance supp package to where the main package will be in A cycles. + $next_bill = $sdate; + for (1..$ratio) { + $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq ); + } + } else { + # harder case: main package is X months, supp package is Y months. + # advance supp package by Y months. then if they're within half a + # month of each other, resync them. this may result in the period + # not being exactly Y months. + $next_bill = $part_pkg->add_freq( $sdate, $supp_pkg_freq ); + my $main_next_bill = $main_pkg->bill; + if ( $main_pkg->bill <= $time ) { + # then the main package has not yet been billed on this cycle; + # predict what its bill date will be. + $main_next_bill = + $part_pkg->add_freq( $main_next_bill, $main_pkg_freq ); + } + if ( abs($main_next_bill - $next_bill) < 86400*15 ) { + $next_bill = $main_next_bill; + } } } else { - # the normal case + # the normal case, not a supplemental package $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); return "unparsable frequency: ". $part_pkg->freq if $next_bill == -1; diff --git a/FS/FS/part_pkg_link.pm b/FS/FS/part_pkg_link.pm index ce071ef17..5fe6f2f01 100644 --- a/FS/FS/part_pkg_link.pm +++ b/FS/FS/part_pkg_link.pm @@ -250,12 +250,10 @@ sub check { my $dst_pkg = $self->dst_pkg; if ( $src_pkg->freq eq '0' and $dst_pkg->freq ne '0' ) { return "One-time charges can't have supplemental packages." - } elsif ( $dst_pkg->freq ne '0' ) { - my $ratio = $dst_pkg->freq / $src_pkg->freq; - if ($ratio != int($ratio)) { - return "Supplemental package period (pkgpart ".$dst_pkg->pkgpart. - ") must be an integer multiple of main package period."; - } + } elsif ( $dst_pkg->freq == 0 ) { + return "The billing period of a supplemental package must be a whole number of months."; + } elsif ( $src_pkg->freq == 0 ) { + return "To have supplemental packages, the billing period of a package must be a whole number of months."; } } -- cgit v1.2.1 From a63a7c017dd67aba394bd97605aa12b9321542f7 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 15 Jul 2015 13:13:44 -0700 Subject: let selfservice users view tickets linked to their services, #33241 --- FS/FS/TicketSystem/RT_Internal.pm | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) (limited to 'FS') diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index d0913d84d..6fb2c187d 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -454,23 +454,21 @@ sub get_ticket_object { my $self = shift; my ($session, %opt) = @_; $session = $self->session(shift); - my $Ticket = RT::Ticket->new($session->{CurrentUser}); - $Ticket->Load($opt{'ticket_id'}); - return if ( !$Ticket->id ); - my $custnum = $opt{'custnum'}; - if ( defined($custnum) && $custnum =~ /^\d+$/ ) { - # probably the most efficient way to check ticket ownership - my $Link = RT::Link->new($session->{CurrentUser}); - $Link->LoadByCols( LocalBase => $opt{'ticket_id'}, - Type => 'MemberOf', - Target => "freeside://freeside/cust_main/$custnum", - ); - return if ( !$Link->id ); + # use a small search here so we can check ticket ownership + my $query; + if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) { + $query = "id = $1"; + } else { + return; + } + if ( $opt{'custnum'} =~ /^(\d+)$/ ) { + $query .= " AND Customer.number = $1"; # also checks ownership via services } - return $Ticket; + my $Tickets = RT::Tickets->new($session->{CurrentUser}); + $Tickets->FromSQL($query); + return $Tickets->First; } - =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ... Class method. Correspond on a ticket. If there is an error, returns the scalar -- cgit v1.2.1 From 5eb1ba22744c6fd98c8a47a3923794a1591122a9 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Wed, 15 Jul 2015 16:33:06 -0700 Subject: set locale in new_customer/new_customer_minimal, RT#34945 --- FS/FS/ClientAPI/Signup.pm | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'FS') diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index 8272b2085..c0a9d98ce 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -539,6 +539,8 @@ sub new_customer { paystart_month paystart_year payissue payip + locale + referral_custnum comments ) ), @@ -946,6 +948,8 @@ sub new_customer_minimal { payinfo paycvv paydate payname paystate paytype paystart_month paystart_year payissue payip + + locale ), } ); -- cgit v1.2.1 From edba2cf4853c062f7f8bf4a41cd429559b548ffc Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 15 Jul 2015 22:07:27 -0500 Subject: RT#37125: Include discounts in report: customer accounting summary --- FS/FS/Report/Table.pm | 58 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 0a0d24a79..ffa117240 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -272,17 +272,53 @@ sub netrefunds { sub discounted { my( $self, $speriod, $eperiod, $agentnum, %opt) = @_; - $self->scalar_sql('SELECT SUM(cust_bill_pkg_discount.amount) - FROM cust_bill_pkg_discount - JOIN cust_bill_pkg USING ( billpkgnum ) - JOIN cust_bill USING ( invnum ) - JOIN cust_main USING ( custnum ) - WHERE '. $self->in_time_period_and_agent( $speriod, - $eperiod, - $agentnum, - 'cust_bill._date' - ). - $self->for_opts(%opt) + + my $sql = 'SELECT SUM('; + if ($opt{'setuprecur'}) { + $sql .= < 0) + OR (COALESCE(cust_bill_pkg.unitrecur,0) > 0)) + THEN +EOF + if ($opt{'setuprecur'} eq 'setup') { + $sql .= ' (COALESCE(cust_bill_pkg.unitsetup,0)'; + } elsif ($opt{'setuprecur'} eq 'recur') { + $sql .= ' (COALESCE(cust_bill_pkg.unitrecur,0)'; + } else { + die 'Unrecognized value for setuprecur'; + } + $sql .= ' / (COALESCE(cust_bill_pkg.unitsetup,0) + COALESCE(cust_bill_pkg.unitrecur,0)))'; + $sql .= " * cust_bill_pkg_discount.amount\n"; + $sql .= <scalar_sql( + $sql + . 'WHERE ' + . $self->in_time_period_and_agent( $speriod, + $eperiod, + $agentnum, + 'cust_bill._date' + ) + . $self->for_opts(%opt) ); } -- cgit v1.2.1 From 1e64160a779e96d84c5db648622dee4f88f34215 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 17 Jul 2015 03:50:54 -0500 Subject: RT#37165: Print document when account is created --- FS/FS/Conf.pm | 10 ++++++++-- FS/FS/svc_acct.pm | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 1d4a85a57..90b5d2985 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2564,11 +2564,17 @@ and customer address. Include units.', 'description' => 'Template to use for welcome messages when a svc_acct record is created.', %msg_template_options, }, - + + { 'key' => 'svc_acct_welcome_letter', + 'section' => 'notification', + 'description' => 'Optional LaTex template for a printed welcome letter when a svc_acct record is created. Fields from the customer and svc_acct records are available for substitution.', + 'type' => 'textarea', + }, + { 'key' => 'svc_acct_welcome_exclude', 'section' => 'notification', - 'description' => 'A list of svc_acct services for which no welcome email is to be sent.', + 'description' => 'A list of svc_acct services for which no welcome email or letter is to be sent.', 'type' => 'select-part_svc', 'multiple' => 1, }, diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 0181b1e0e..a76d93dab 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -57,6 +57,7 @@ use FS::svc_forward; use FS::svc_www; use FS::cdr; use FS::tower_sector; +use FS::Misc; $DEBUG = 0; $me = '[FS::svc_acct]'; @@ -728,9 +729,11 @@ sub insert { $cust_main->invoicing_list(\@invoicing_list); } - #welcome email + #welcome email/letter my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude'); unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) { + #indent skips a level for some reason + #welcome email my $error = ''; my $msgnum = $conf->config('welcome_msgnum', $agentnum); if ( $msgnum ) { @@ -814,7 +817,21 @@ sub insert { } # if $welcome_template } # if !$msgnum - } + # print welcome letter + if ($conf->exists('svc_acct_welcome_letter')) { + my $queue = new FS::queue { + 'job' => 'FS::svc_acct::process_print_welcome_letter', + }; + $error = $queue->insert( + 'svcnum' => $self->svcnum, + 'template' => 'svc_acct_welcome_letter', + ); + if ($error) { + warn "can't send welcome letter: $error"; + } + } + #indent skipped a level for some reason + } # unless in @welcome_exclude_svcparts } # if $cust_pkg $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -3019,6 +3036,26 @@ sub reached_threshold { } } +sub process_print_welcome_letter { + my %opt = @_; + + my $self = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } ) + or die "invalid svc_acct: " . $opt{'svcnum'}; + my $cust_main = $self->cust_svc->cust_pkg->cust_main; + + my $ps = $cust_main->print_ps('svc_acct_welcome_letter', + 'extra_fields' => { + map { $_ => $self->$_ } $self->fields, # or maybe just username & password? + }, + ); + my $error = FS::Misc::do_print( + [ $ps ], + 'agentnum' => $cust_main->agentnum, + ); + die $error if $error; + +} + =back =head1 BUGS -- cgit v1.2.1 From cf90a22a09d15140168f8232ccea788c36af71f2 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 17 Jul 2015 15:08:57 -0700 Subject: fix invoice reprinting + cust_bill::Search changes, #37320, from #25943 --- FS/FS/cust_bill.pm | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 5052ed130..6a4fa1e8f 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2974,6 +2974,9 @@ sub process_re_X { } +# this is called from search/cust_bill.html and given all its search +# parameters, so it needs to perform the same search. + sub re_X { # spool_invoice ftp_invoice fax_invoice print_invoice my($method, $job, %param ) = @_; @@ -2983,22 +2986,15 @@ sub re_X { } #some false laziness w/search/cust_bill.html - my $distinct = ''; - my $orderby = 'ORDER BY cust_bill._date'; - - my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param); - - my $addl_from = 'LEFT JOIN cust_main USING ( custnum )'; - - my @cust_bill = qsearch( { - #'select' => "cust_bill.*", - 'table' => 'cust_bill', - 'addl_from' => $addl_from, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, - 'debug' => 1, - } ); + $param{'order_by'} = 'cust_bill._date'; + + my $query = FS::cust_bill->search(\%param); + delete $query->{'count_query'}; + delete $query->{'count_addl'}; + + $query->{debug} = 1; # was in here before, is obviously useful + + my @cust_bill = qsearch( $query ); $method .= '_invoice' unless $method eq 'email' || $method eq 'print'; -- cgit v1.2.1 From de4008c1c673fbb50e8b2806126fe9e7bb8ec0ee Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 17 Jul 2015 15:20:37 -0700 Subject: also fix searching by charged/owed amount --- FS/FS/cust_bill/Search.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/cust_bill/Search.pm b/FS/FS/cust_bill/Search.pm index 925938d82..ee5da3be8 100644 --- a/FS/FS/cust_bill/Search.pm +++ b/FS/FS/cust_bill/Search.pm @@ -276,7 +276,7 @@ sub search_sql_where { my @owed = ref($param->{owed}) ? @{ $param->{owed} } : ($param->{owed}); - push @search, map { s/^owed/$owed_sql/ } @owed; + push @search, map { s/^owed/$owed_sql/; $_ } @owed; } #open/net flags -- cgit v1.2.1 From bc68d45408c963fbf7c024c15198373dc6b1de8f Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 17 Jul 2015 23:40:53 -0500 Subject: RT#37165: Print document when account is created [implemented as export] --- FS/FS/Conf.pm | 10 +- FS/FS/cust_main.pm | 11 +- FS/FS/part_export/print_template.pm | 198 ++++++++++++++++++++++++++++++++++++ FS/FS/svc_acct.pm | 41 +------- 4 files changed, 211 insertions(+), 49 deletions(-) create mode 100644 FS/FS/part_export/print_template.pm (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 90b5d2985..1d4a85a57 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2564,17 +2564,11 @@ and customer address. Include units.', 'description' => 'Template to use for welcome messages when a svc_acct record is created.', %msg_template_options, }, - - { 'key' => 'svc_acct_welcome_letter', - 'section' => 'notification', - 'description' => 'Optional LaTex template for a printed welcome letter when a svc_acct record is created. Fields from the customer and svc_acct records are available for substitution.', - 'type' => 'textarea', - }, - + { 'key' => 'svc_acct_welcome_exclude', 'section' => 'notification', - 'description' => 'A list of svc_acct services for which no welcome email or letter is to be sent.', + 'description' => 'A list of svc_acct services for which no welcome email is to be sent.', 'type' => 'select-part_svc', 'multiple' => 1, }, diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f55c3493c..f0c4f5c0b 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4681,6 +4681,8 @@ I - a hashref of name/value pairs which will be substituted into the template. These values may override values mentioned below and those from the customer record. +I - if present, ignores TEMPLATE_NAME and uses the provided text + The following variables are available in the template instead of or in addition to the fields of the customer record. @@ -4696,11 +4698,16 @@ I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or sub generate_letter { my ($self, $template, %options) = @_; - return unless $conf->exists($template); + warn "Template $template does not exist" && return + unless $conf->exists($template) || $options{'template_text'}; + + my $template_source = $options{'template_text'} + ? [ $options{'template_text'} ] + : [ map "$_\n", $conf->config($template) ]; my $letter_template = new Text::Template ( TYPE => 'ARRAY', - SOURCE => [ map "$_\n", $conf->config($template)], + SOURCE => $template_source, DELIMITERS => [ '[@--', '--@]' ], ) or die "can't create new Text::Template object: Text::Template::ERROR"; diff --git a/FS/FS/part_export/print_template.pm b/FS/FS/part_export/print_template.pm new file mode 100644 index 000000000..21f0a152e --- /dev/null +++ b/FS/FS/part_export/print_template.pm @@ -0,0 +1,198 @@ +package FS::part_export::print_template; + +use strict; + +use base qw( FS::part_export ); + +use FS::Record qw(qsearchs); +use FS::Misc; +use FS::queue; + +=pod + +=head1 NAME + +FS::part_export::print_template + +=head1 SYNOPSIS + +Print a document of a template. + +=head1 DESCRIPTION + +See the L documentation and the billing documentation for details on the template substitution language. + +Currently does not support printing during replace. + +=cut + +use vars qw( %info ); + +tie my %options, 'Tie::IxHash', + 'phase' => { label => 'Print during', + type => 'select', + options => [qw(insert delete suspend unsuspend)] }, + 'template_text' => { label => 'Template text', + type => 'textarea' }, +; + +%info = ( + #unfortunately, FS::part_svc->svc_tables fails at this point, not sure why + 'svc' => [ map { 'svc_'.$_ } qw( + acct domain cert forward mailinglist www broadband cable dsl + conferencing video dish hardware phone pbx circuit port alarm external) + ], + 'desc' => 'Print document during service change, for all services', + 'options' => \%options, + 'no_machine' => 1, + 'notes' => <<'EOF', +Will use the print command configured by the lpr setting. +See the Text::Template documentation and the billing documentation for details on the template substitution language. +Fields from the customer and service records are available for substitution, as well as the following fields: + +
    +
  • $payby - a friendler represenation of the field
  • +
  • $payinfo - the masked payment information
  • +
  • $expdate - the time at which the payment method expires (a UNIX timestamp)
  • +
  • $returnaddress - the invoice return address for this customer's agent
  • +
  • $logo_file - the image stored in the logo.eps setting +
+EOF +); + +=head1 Hook Methods + +Each of these simply invoke this module's L method, +passing the appropriate phase. + +=cut + +=head2 _export_insert + +Hook that is called when service is initially provisioned. +To avoid confusion, don't use for anything else. + +=cut + +sub _export_insert { + my $self = shift; + return $self->print_template('insert',@_); +} + +=head2 _export_delete + +Hook that is called when service is unprovisioned. +To avoid confusion, don't use for anything else. + +=cut + +sub _export_delete { + my $self = shift; + return $self->print_template('delete',@_); +} + +=head2 _export_replace + +Hook that is called when provisioned service is edited. +To avoid confusion, don't use for anything else. + +Currently not supported for this export. + +=cut + +sub _export_replace { + return ''; +} + +=head2 _export_suspend + +Hook that is called when service is suspended. +To avoid confusion, don't use for anything else. + +=cut + +sub _export_suspend { + my $self = shift; + return $self->print_template('suspend',@_); +} + +=head2 _export_unsuspend + +Hook that is called when service is unsuspended. +To avoid confusion, don't use for anything else. + +=cut + +sub _export_unsuspend { + my $self = shift; + return $self->print_template('unsuspend',@_); +} + +=head1 Core Methods + +=head2 print_template + +Accepts $phase and $svc_x. +If phase matches the configured option, starts a L +job in the queue. + +=cut + +sub print_template { + my ($self, $phase, $svc_x) = @_; + if ($self->option('phase') eq $phase) { + my $queue = new FS::queue { + 'job' => 'FS::part_export::print_template::process_print_template', + }; + my $error = $queue->insert( + 'svcnum' => $svc_x->svcnum, + 'table' => $svc_x->table, + 'template_text' => $self->option('template_text'), + ); + return "can't start print job: $error" if $error; + } + return ''; +} + +=head2 process_print_template + +For use as an FS::queue job. Requires opts svcnum, table and template_text. +Constructs page from template and sends to printer. + +=cut + +sub process_print_template { + my %opt = @_; + + my $svc_x = qsearchs($opt{'table'}, { 'svcnum' => $opt{'svcnum'} } ) + or die "invalid " . $opt{'table'} . " svcnum " . $opt{'svcnum'}; + my $cust_main = $svc_x->cust_svc->cust_pkg->cust_main + or die "could not find customer for service"; + + my $ps = $cust_main->print_ps(undef, + 'template_text' => $opt{'template_text'}, + 'extra_fields' => { + map { $_ => $svc_x->$_ } $svc_x->fields, + }, + ); + my $error = FS::Misc::do_print( + [ $ps ], + 'agentnum' => $cust_main->agentnum, + ); + die $error if $error; +} + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Jonathan Prykop +jonathan@freeside.biz + +=cut + +1; + + diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index a76d93dab..0181b1e0e 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -57,7 +57,6 @@ use FS::svc_forward; use FS::svc_www; use FS::cdr; use FS::tower_sector; -use FS::Misc; $DEBUG = 0; $me = '[FS::svc_acct]'; @@ -729,11 +728,9 @@ sub insert { $cust_main->invoicing_list(\@invoicing_list); } - #welcome email/letter + #welcome email my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude'); unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) { - #indent skips a level for some reason - #welcome email my $error = ''; my $msgnum = $conf->config('welcome_msgnum', $agentnum); if ( $msgnum ) { @@ -817,21 +814,7 @@ sub insert { } # if $welcome_template } # if !$msgnum - # print welcome letter - if ($conf->exists('svc_acct_welcome_letter')) { - my $queue = new FS::queue { - 'job' => 'FS::svc_acct::process_print_welcome_letter', - }; - $error = $queue->insert( - 'svcnum' => $self->svcnum, - 'template' => 'svc_acct_welcome_letter', - ); - if ($error) { - warn "can't send welcome letter: $error"; - } - } - #indent skipped a level for some reason - } # unless in @welcome_exclude_svcparts + } } # if $cust_pkg $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -3036,26 +3019,6 @@ sub reached_threshold { } } -sub process_print_welcome_letter { - my %opt = @_; - - my $self = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } ) - or die "invalid svc_acct: " . $opt{'svcnum'}; - my $cust_main = $self->cust_svc->cust_pkg->cust_main; - - my $ps = $cust_main->print_ps('svc_acct_welcome_letter', - 'extra_fields' => { - map { $_ => $self->$_ } $self->fields, # or maybe just username & password? - }, - ); - my $error = FS::Misc::do_print( - [ $ps ], - 'agentnum' => $cust_main->agentnum, - ); - die $error if $error; - -} - =back =head1 BUGS -- cgit v1.2.1 From 0b9712428c18bd68f39c66a18e489c0e0ebc79af Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 17 Jul 2015 23:41:51 -0500 Subject: RT#37165: Print document when account is created [fix for welcome_letter, no backport to 3] --- FS/FS/cust_main.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f0c4f5c0b..c636408d8 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -4822,7 +4822,9 @@ sub queueable_print { my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } ) or die "invalid customer number: " . $opt{custnum}; - my $error = $self->print( { 'template' => $opt{template} } ); +#do not backport this change to 3.x +# my $error = $self->print( { 'template' => $opt{template} } ); + my $error = $self->print( $opt{'template'} ); die $error if $error; } -- cgit v1.2.1 From f97cd91298d3cbfd380741cf4a64d402d3628ecd Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Mon, 20 Jul 2015 17:18:36 -0700 Subject: remove debug --- FS/FS/ClientAPI/MyAccount/quotation.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount/quotation.pm b/FS/FS/ClientAPI/MyAccount/quotation.pm index df2b37ed6..667f37d78 100644 --- a/FS/FS/ClientAPI/MyAccount/quotation.pm +++ b/FS/FS/ClientAPI/MyAccount/quotation.pm @@ -5,7 +5,7 @@ use FS::Record qw(qsearch qsearchs); use FS::quotation; use FS::quotation_pkg; -our $DEBUG = 1; +our $DEBUG = 0; sub _custoragent_session_custnum { FS::ClientAPI::MyAccount::_custoragent_session_custnum(@_); -- cgit v1.2.1 From 86b44a7897af8265bcd87603fbf2632489ca10b8 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 20 Jul 2015 19:24:48 -0500 Subject: RT#37125: Include discounts in report: customer accounting summary [some fixes] --- FS/FS/Report/Table.pm | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) (limited to 'FS') diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index ffa117240..63e5318c3 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -229,7 +229,8 @@ sub receipts { #net payments my $sql = 'SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay'; if ( $opt{'setuprecur'} ) { $sql = 'SELECT SUM('. - FS::cust_bill_pkg->paid_sql($speriod, $eperiod, %opt). + #in practice, but not appearance, paid_sql accepts end before start + FS::cust_bill_pkg->paid_sql($eperiod, $speriod, %opt). ') FROM cust_bill_pkg'; } @@ -275,6 +276,11 @@ sub discounted { my $sql = 'SELECT SUM('; if ($opt{'setuprecur'}) { + # (This isn't exact but it works in most cases.) + # When splitting into setup/recur values, + # if the discount is allowed to apply to setup fees (discount.setup = 'Y') + # then split it between the "setup" and "recurring" rows in proportion to + # the "unitsetup" and "unitrecur" fields of the line item. $sql .= < Date: Mon, 20 Jul 2015 19:09:17 -0700 Subject: contact self-service pw changes, RT#37023 --- FS/FS/ClientAPI/MyAccount.pm | 53 ++----------- FS/FS/ClientAPI/MyAccount/contact.pm | 148 +++++++++++++++++++++++++++++++++++ FS/FS/ClientAPI_XMLRPC.pm | 6 ++ FS/MANIFEST | 2 + 4 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 FS/FS/ClientAPI/MyAccount/contact.pm (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index 420ed0688..824ff67cb 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -50,7 +50,9 @@ use FS::cust_contact; use FS::cust_location; use FS::cust_payby; -use FS::ClientAPI::MyAccount::quotation; # just for code organization +# for code organization +use FS::ClientAPI::MyAccount::contact; +use FS::ClientAPI::MyAccount::quotation; $DEBUG = 0; $me = '[FS::ClientAPI::MyAccount]'; @@ -243,6 +245,8 @@ sub login { return { error => 'Incorrect contact password.' } unless $contact->authenticate_password($p->{'password'}); + $session->{'contactnum'} = $contact->contactnum; + my @cust_contact = grep $_->selfservice_access, $contact->cust_contact; if ( scalar(@cust_contact) == 1 ) { $session->{'custnum'} = $cust_contact[0]->custnum; @@ -3002,53 +3006,6 @@ sub myaccount_passwd { } -# sub contact_passwd { -# my $p = shift; -# my($context, $session, $custnum) = _custoragent_session_custnum($p); -# return { 'error' => $session } if $context eq 'error'; -# -# return { 'error' => 'Not logged in as a contact.' } -# unless $session->{'contactnum'}; -# -# return { 'error' => "New passwords don't match." } -# if $p->{'new_password'} ne $p->{'new_password2'}; -# -# return { 'error' => 'Enter new password' } -# unless length($p->{'new_password'}); -# -# #my $search = { 'custnum' => $custnum }; -# #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; -# $custnum =~ /^(\d+)$/ or die "illegal custnum"; -# my $search = " AND selfservice_access IS NOT NULL ". -# " AND selfservice_access = 'Y' ". -# " AND ( disabled IS NULL OR disabled = '' )". -# " AND custnum IS NOT NULL AND custnum = $1"; -# $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent'; -# -# my $contact = qsearchs( { -# 'table' => 'contact', -# 'addl_from' => 'LEFT JOIN cust_main USING ( custnum ) ', -# 'hashref' => { 'contactnum' => $session->{'contactnum'}, }, -# 'extra_sql' => $search, #important -# } ) -# or return { 'error' => "Email not found" }; #? how did we get logged in? -# # deleted since then? -# -# my $error = ''; -# -# # use these svc_acct length restrictions?? -# my $conf = new FS::Conf; -# $error = 'Password too short.' -# if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6); -# $error = 'Password too long.' -# if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8); -# -# $error ||= $contact->change_password($p->{'new_password'}); -# -# return { 'error' => $error, }; -# -# } - sub reset_passwd { my $p = shift; diff --git a/FS/FS/ClientAPI/MyAccount/contact.pm b/FS/FS/ClientAPI/MyAccount/contact.pm new file mode 100644 index 000000000..72226e2dc --- /dev/null +++ b/FS/FS/ClientAPI/MyAccount/contact.pm @@ -0,0 +1,148 @@ +package FS::ClientAPI::MyAccount::contact; + +use strict; +use FS::Record qw( qsearchs ); +use FS::cust_main; +use FS::cust_contact; +use FS::contact; + +sub _custoragent_session_custnum { + FS::ClientAPI::MyAccount::_custoragent_session_custnum(@_); +} + +sub contact_passwd { + my $p = shift; + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + return { 'error' => 'Not logged in as a contact.' } + unless $session->{'contactnum'}; + + return { 'error' => 'Enter new password' } + unless length($p->{'new_password'}); + + my $contact = _contact( $session->{'contactnum'}, $custnum ) + or return { 'error' => "Email not found" }; + + my $error = ''; + + # use these svc_acct length restrictions?? + my $conf = new FS::Conf; + $error = 'Password too short.' + if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6); + $error = 'Password too long.' + if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8); + + $error ||= $contact->change_password($p->{'new_password'}); + + return { 'error' => $error }; + +} + +sub _contact { + my( $contactnum, $custnum ) = @_; + + #my $search = { 'custnum' => $custnum }; + #$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent'; + $custnum =~ /^(\d+)$/ or die "illegal custnum"; + my $search = " AND cust_contact.selfservice_access IS NOT NULL ". + " AND cust_contact.selfservice_access = 'Y' ". + " AND ( disabled IS NULL OR disabled = '' )". + " AND cust_contact.custnum IS NOT NULL AND cust_contact.custnum = $1"; +# $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent'; + + qsearchs( { + 'table' => 'contact', + #'addl_from' => 'LEFT JOIN cust_main USING ( custnum ) ', + 'addl_from' => ' LEFT JOIN cust_contact USING ( contactnum ) '. + ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum ) ', + 'hashref' => { 'contactnum' => $contactnum, }, + 'extra_sql' => $search, #important + } ); + +} + +sub list_contacts { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_main = qsearchs('cust_main', { custnum=>$custnum } ); + + my @contacts = ( map { + my $contact = $_->contact; + my @contact_email = $contact->contact_email; + { 'contactnum' => $contact->contactnum, + 'class' => $_->contact_classname, + 'first' => $contact->first, + 'last' => $contact->get('last'), + 'title' => $contact->title, + 'emailaddress' => join(',', map $_->emailaddress, @contact_email), + #TODO: contact phone numbers + 'comment' => $_->comment, + 'selfservice_access' => $_->selfservice_access, + 'disabled' => $contact->disabled, + }; + } $cust_main->cust_contact ); + + return { 'error' => '', + 'contacts' => \@contacts, + }; +} + +sub edit_contact { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + #shortcut: logged in as a contact? that must be the one you want to edit + my $contactnum = $p->{contactnum} || $session->{'contactnum'}; + + my $contact = _contact( $contactnum, $custnum ) + or return { 'error' => "Email not found" }; + + return { error => "Can't edit a multi-customer contact unless logged in as that contact" } + if $contactnum != $session->{'contactnum'} + && scalar( $contact->cust_contact ) > 1; + + #my $cust_contact = qsearchs('cust_contact', { contactnum => $contactnum, + # custnum => $custnum, } ) + # or die "guru meditation #4200"; + + #TODO: change more fields besides just these + + foreach (qw( first last title emailaddress )) { + $contact->$_( $p->{$_} ) if length( $p->{$_} ); + } + + my $error = $contact->replace; + + return { 'error' => $error, }; + +} + +sub delete_contact { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_contact = qsearchs('cust_contact', { contactnum => $p->{contactnum}, + custnum => $custnum, }) + or return { 'error' => 'Unknown contactnum' }; + + my $contact = $cust_contact->contact; + + my $error = $cust_contact->delete; + return { 'error' => $error } if $error; + + unless ( $contact->cust_contact ) { + $contact->delete; + } + + return { 'error' => '', }; +} + +1; diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 04aee290b..8f02b0925 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -106,6 +106,12 @@ sub ss2clientapi { 'switch_cust' => 'MyAccount/switch_cust', 'customer_info' => 'MyAccount/customer_info', 'customer_info_short' => 'MyAccount/customer_info_short', + + 'contact_passwd' => 'MyAccount/contact/contact_passwd', + 'list_contacts' => 'MyAccount/contact/list_contacts', + 'edit_contact' => 'MyAccount/contact/edit_contact', + 'delete_contact' => 'MyAccount/contact/delete_contact', + 'billing_history' => 'MyAccount/billing_history', 'edit_info' => 'MyAccount/edit_info', #add to ss cgi! 'invoice' => 'MyAccount/invoice', diff --git a/FS/MANIFEST b/FS/MANIFEST index 899270bf2..5b73b728c 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -39,6 +39,8 @@ FS/ClientAPI/passwd.pm FS/ClientAPI/Agent.pm FS/ClientAPI/MasonComponent.pm FS/ClientAPI/MyAccount.pm +FS/ClientAPI/MyAccount/contact.pm +FS/ClientAPI/MyAccount/quotation.pm FS/ClientAPI/PrepaidPhone.pm FS/ClientAPI/Signup.pm FS/Conf.pm -- cgit v1.2.1 From d83a8bf58a425da397472cc0549f98d906365dba Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 11:05:17 -0700 Subject: self-service delete_contact, RT#37375 --- FS/FS/ClientAPI/MyAccount/contact.pm | 3 +++ 1 file changed, 3 insertions(+) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount/contact.pm b/FS/FS/ClientAPI/MyAccount/contact.pm index 72226e2dc..099da29e0 100644 --- a/FS/FS/ClientAPI/MyAccount/contact.pm +++ b/FS/FS/ClientAPI/MyAccount/contact.pm @@ -129,6 +129,9 @@ sub delete_contact { my($context, $session, $custnum) = _custoragent_session_custnum($p); return { 'error' => $session } if $context eq 'error'; + return { 'error' => 'Cannot delete the currently-logged in contact.' } + if $p->{contactnum} == $session->{contactnum}; + my $cust_contact = qsearchs('cust_contact', { contactnum => $p->{contactnum}, custnum => $custnum, }) or return { 'error' => 'Unknown contactnum' }; -- cgit v1.2.1 From 8e64188a4f9886971da617a1bde869d5725f82c8 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 11:32:57 -0700 Subject: self-service API new_contact, RT#37376 --- FS/FS/ClientAPI/MyAccount/contact.pm | 21 +++++++++++++++++++++ FS/FS/ClientAPI_XMLRPC.pm | 1 + 2 files changed, 22 insertions(+) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount/contact.pm b/FS/FS/ClientAPI/MyAccount/contact.pm index 099da29e0..5690f867b 100644 --- a/FS/FS/ClientAPI/MyAccount/contact.pm +++ b/FS/FS/ClientAPI/MyAccount/contact.pm @@ -148,4 +148,25 @@ sub delete_contact { return { 'error' => '', }; } +sub new_contact { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + #TODO: add phone numbers too + #TODO: specify a classnum by name and/or list_contact_classes method + + my $contact = new FS::contact { + 'custnum' => $custnum, + map { $_ => $p->{$_} } + qw( first last emailaddress classnum comment selfservice_access ) + }; + + $contact->change_password_fields($p->{_password}) if length($p->{_password}); + + my $error = $contact->insert; + return { 'error' => $error, }; +} + 1; diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 8f02b0925..435ee9835 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -111,6 +111,7 @@ sub ss2clientapi { 'list_contacts' => 'MyAccount/contact/list_contacts', 'edit_contact' => 'MyAccount/contact/edit_contact', 'delete_contact' => 'MyAccount/contact/delete_contact', + 'new_contact' => 'MyAccount/contact/new_contact', 'billing_history' => 'MyAccount/billing_history', 'edit_info' => 'MyAccount/edit_info', #add to ss cgi! -- cgit v1.2.1 From 71d389e6554fd9994ac9c18bc59fd43449b8cca1 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 15:16:12 -0700 Subject: remove invoice deletion (and ancient & unused config settings), RT#37157 --- FS/FS/Conf.pm | 125 ----------------------------------------------------- FS/FS/cust_bill.pm | 11 +++-- 2 files changed, 5 insertions(+), 131 deletions(-) (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 1d4a85a57..4e1736be3 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -719,13 +719,6 @@ my $validate_email = sub { $_[0] =~ @config_items = map { new FS::ConfItem $_ } ( - { - 'key' => 'address', - 'section' => 'deprecated', - 'description' => 'This configuration option is no longer used. See invoice_template instead.', - 'type' => 'text', - }, - { 'key' => 'event_log_level', 'section' => 'notification', @@ -1045,13 +1038,6 @@ my $validate_email = sub { $_[0] =~ 'per_locale' => 1, }, - { - 'key' => 'deleteinvoices', - 'section' => 'UI', - 'description' => 'Enable invoices deletions. Be very careful! Deleting an invoice will remove all traces that the invoice ever existed! Normally, you would void or apply a credit against the invoice instead.', - 'type' => 'checkbox', - }, - { 'key' => 'deletecredits', #not actually deprecated yet @@ -1069,20 +1055,6 @@ my $validate_email = sub { $_[0] =~ 'type' => 'checkbox', }, - { - 'key' => 'unapplypayments', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable "unapplication" of unclosed payments.', - 'type' => 'checkbox', - }, - - { - 'key' => 'unapplycredits', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable "unapplication" of unclosed credits.', - 'type' => 'checkbox', - }, - { 'key' => 'dirhash', 'section' => 'shell', @@ -1669,13 +1641,6 @@ and customer address. Include units.', 'type' => 'checkbox', }, - { - 'key' => 'invoice_send_receipts', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, this used to send an invoice copy on payments and credits. See the payment_receipt_email and XXXX instead.', - 'type' => 'checkbox', - }, - { 'key' => 'payment_receipt', 'section' => 'notification', @@ -1873,13 +1838,6 @@ and customer address. Include units.', # 'description' => 'Directory which contains domain registry information. Each registry is a directory.', # }, - { - 'key' => 'report_template', - 'section' => 'deprecated', - 'description' => 'Deprecated template file for reports.', - 'type' => 'textarea', - }, - { 'key' => 'maxsearchrecordsperpage', 'section' => 'UI', @@ -2686,13 +2644,6 @@ and customer address. Include units.', 'type' => 'checkbox', }, - { - 'key' => 'paymentforcedtobatch', - 'section' => 'deprecated', - 'description' => 'See batch-enable_payby and realtime-disable_payby. Used to (for CHEK): Cause per customer payment entry to be forced to a batch processor rather than performed realtime.', - 'type' => 'checkbox', - }, - { 'key' => 'svc_acct-notes', 'section' => 'deprecated', @@ -2767,13 +2718,6 @@ and customer address. Include units.', 'validate' => $validate_email, }, - { - 'key' => 'users-allow_comp', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, enable the Complimentary customer access right instead. Was: Usernames (Freeside users, created with freeside-adduser) which can create complimentary customers, one per line. If no usernames are entered, all users can create complimentary accounts.', - 'type' => 'textarea', - }, - { 'key' => 'credit_card-recurring_billing_flag', 'section' => 'billing', @@ -3382,27 +3326,6 @@ and customer address. Include units.', 'per_agent' => 1, }, - { - 'key' => 'echeck-void', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable local-only voiding of echeck payments in addition to refunds against the payment gateway', - 'type' => 'checkbox', - }, - - { - 'key' => 'cc-void', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable local-only voiding of credit card payments in addition to refunds against the payment gateway', - 'type' => 'checkbox', - }, - - { - 'key' => 'unvoid', - 'section' => 'deprecated', - 'description' => 'DEPRECATED, now controlled by ACLs. Used to enable unvoiding of voided payments', - 'type' => 'checkbox', - }, - { 'key' => 'address1-search', 'section' => 'UI', @@ -3432,12 +3355,6 @@ and customer address. Include units.', 'per_agent' => 1, }, - { 'key' => 'referral_credit', - 'section' => 'deprecated', - 'description' => "Used to enable one-time referral credits in the amount of one month referred customer's recurring fee (irregardless of frequency). Replace with a billing event on appropriate packages.", - 'type' => 'checkbox', - }, - { 'key' => 'selfservice_server-cache_module', 'section' => 'self-service', 'description' => 'Module used to store self-service session information. All modules handle any number of self-service servers. Cache::SharedMemoryCache is appropriate for a single database / single Freeside server. Cache::FileCache is useful for multiple databases on a single server, or when IPC::ShareLite is not available (i.e. FreeBSD).', # _Database stores session information in the database and is appropriate for multiple Freeside servers, but may be slower.', @@ -5998,48 +5915,6 @@ and customer address. Include units.', 'type' => 'text', }, - { key => "apacheroot", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "apachemachine", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "apachemachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "bindprimary", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "bindsecondaries", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "bsdshellmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "cyrus", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "cp_app", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "erpcdmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "icradiusmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "icradius_mysqldest", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "icradius_mysqlsource", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "icradius_secrets", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "maildisablecatchall", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "mxmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "nsmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "arecords", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "cnamerecords", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "nismachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "qmailmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "radiusmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "sendmailconfigpath", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "sendmailmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "sendmailrestart", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "shellmachine", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "shellmachine-useradd", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "shellmachine-userdel", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "shellmachine-usermod", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "shellmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "radiusprepend", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "textradiusprepend", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "username_policy", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "vpopmailmachines", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "vpopmailrestart", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "safe-part_pkg", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "selfservice_server-quiet", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "signup_server-quiet", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "signup_server-email", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "vonage-username", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "vonage-password", section => "deprecated", description => "DEPRECATED", type => "text" }, - { key => "vonage-fromnumber", section => "deprecated", description => "DEPRECATED", type => "text" }, - ); 1; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 6a4fa1e8f..b694924f3 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -267,14 +267,13 @@ sub void { =item delete -This method now works but you probably shouldn't use it. Instead, apply a -credit against the invoice, or use the new void method. +DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use +the B method. -Using this method to delete invoices outright is really, really bad. There -would be no record you ever posted this invoice, and there are no check to -make sure charged = 0 or that there are no associated cust_bill_pkg records. +This is only for internal use by V, which is what you should be using. -Really, don't use it. +DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly +wrong. Use B, that's what it is for. Really. This means you. =cut -- cgit v1.2.1 From 164e0da4c66b354bb35bf0af54a1f28555f0bf2a Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 15:25:01 -0700 Subject: remove external cust_bill::delete method, RT#37157 --- FS/FS/cust_bill.pm | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) (limited to 'FS') diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index b694924f3..7ea586a90 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -253,7 +253,7 @@ sub void { } } - $error = $self->delete; + $error = $self->_delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -265,19 +265,22 @@ sub void { } -=item delete - -DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use -the B method. - -This is only for internal use by V, which is what you should be using. - -DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly -wrong. Use B, that's what it is for. Really. This means you. - -=cut - -sub delete { +# removed docs entirely and renamed method to _delete to further indicate it is +# internal-only and discourage use +# +# =item delete +# +# DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use +# the B method. +# +# This is only for internal use by V, which is what you should be using. +# +# DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly +# wrong. Use B, that's what it is for. Really. This means you. +# +# =cut + +sub _delete { my $self = shift; return "Can't delete closed invoice" if $self->closed =~ /^Y/i; -- cgit v1.2.1 From 4e63920a6b29e4e0b2321f80427211816d697612 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 15:25:34 -0700 Subject: remove "Delete invoices" ACL, RT#37157 --- FS/FS/AccessRight.pm | 2 -- 1 file changed, 2 deletions(-) (limited to 'FS') diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 0493edd33..9274ad858 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -184,7 +184,6 @@ tie my %rights, 'Tie::IxHash', 'Resend invoices', #NEWNEW 'Void invoices', 'Unvoid invoices', - 'Delete invoices', 'View customer tax exemptions', #yow 'Edit customer tax exemptions', #NEWNEW 'Add customer tax adjustment', #new, but no need to phase in @@ -441,7 +440,6 @@ Most (but not all) right names. sub default_superuser_rights { my $class = shift; my %omit = map { $_=>1 } ( - 'Delete invoices', 'Delete payment', 'Delete credit', #? 'Delete refund', #? -- cgit v1.2.1 From c39d6e73a98427d653e3d019aaa90f16bc1b6380 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 19:16:06 -0700 Subject: self-service API list_contacts doc, RT#37372 --- FS/FS/ClientAPI/MyAccount/contact.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount/contact.pm b/FS/FS/ClientAPI/MyAccount/contact.pm index 5690f867b..31bbb0cb1 100644 --- a/FS/FS/ClientAPI/MyAccount/contact.pm +++ b/FS/FS/ClientAPI/MyAccount/contact.pm @@ -82,7 +82,7 @@ sub list_contacts { #TODO: contact phone numbers 'comment' => $_->comment, 'selfservice_access' => $_->selfservice_access, - 'disabled' => $contact->disabled, + #'disabled' => $contact->disabled, }; } $cust_main->cust_contact ); -- cgit v1.2.1 From ec2f19b4484371dff04ec4a5313c7b7d2359cca8 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Tue, 21 Jul 2015 19:17:21 -0700 Subject: self-service API contact deletion fix, RT#37375 --- FS/FS/ClientAPI/MyAccount/contact.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/ClientAPI/MyAccount/contact.pm b/FS/FS/ClientAPI/MyAccount/contact.pm index 31bbb0cb1..ff29079c7 100644 --- a/FS/FS/ClientAPI/MyAccount/contact.pm +++ b/FS/FS/ClientAPI/MyAccount/contact.pm @@ -141,7 +141,7 @@ sub delete_contact { my $error = $cust_contact->delete; return { 'error' => $error } if $error; - unless ( $contact->cust_contact ) { + unless ( $contact->cust_contact || $contact->prospect_contact ) { $contact->delete; } -- cgit v1.2.1 From 23cf073d5505dc77f7bdc1c7ce4c2364830357ed Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 21 Jul 2015 17:41:01 -0700 Subject: Broadworks: fix incomplete changing of phone numbers, #25927 --- FS/FS/part_export/broadworks.pm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/part_export/broadworks.pm b/FS/FS/part_export/broadworks.pm index 59668672c..a04a70e9b 100644 --- a/FS/FS/part_export/broadworks.pm +++ b/FS/FS/part_export/broadworks.pm @@ -41,7 +41,8 @@ number, this requires a svc_phone definition and a part_device. The "title" field ("external name") of the part_device must be one of the access device type names recognized by BroadWorks, such as "Polycom Soundpoint IP 550", "SNOM 320", or "Generic SIP Phone".

-

+

Each phone service must have a device linked before it will be functional. +Until then, authentication will be denied.

END ); @@ -85,6 +86,11 @@ sub export_replace { newUserId => $newUserId ); return $message if !$success; + + if ( my $device = qsearchs('phone_device', { svcnum => $svc_new->svcnum }) ) { + # there's a Line/Port configured for the device, and it also needs to be renamed. + $error ||= $self->set_endpoint( $newUserId, $self->deviceName($device) ); + } } if ( $svc_old->phonenum ne $svc_new->phonenum ) { -- cgit v1.2.1 From 6a5c6931db91ef135b212cd679f1e296708333fd Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 22 Jul 2015 16:25:32 -0500 Subject: RT#37163: Disconnect Users via Radclient --- FS/FS/part_export/sqlradius.pm | 125 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index d1387d636..1f5b018ce 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -73,6 +73,16 @@ tie %options, 'Tie::IxHash', type => 'checkbox', label => 'Export RADIUS group attributes to this database', }, + 'disconnect_ssh' => { + label => 'To send a disconnection request to each RADIUS client when modifying, suspending or deleting an account, enter a ssh connection string (username@host) with access to the radclient program', + }, + 'disconnect_port' => { + label => 'Port to send disconnection requests to, default 1700', + }, + 'disconnect_log' => { + label => 'Print disconnect output and errors to the queue log (will otherwise fail silently)', + type => 'checkbox', + }, ; $notes1 = <<'END'; @@ -184,6 +194,22 @@ sub _export_replace { my $dbh = dbh; my $jobnum = ''; + + # disconnect users before changing username + if ($self->option('disconnect_ssh')) { + my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', + 'disconnect_ssh' => $self->option('disconnect_ssh'), + 'svc_acct_username' => $old->username, + 'disconnect_port' => $self->option('disconnect_port'), + 'disconnect_log' => $self->option('disconnect_log'), + ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + $jobnum = $err_or_queue->jobnum; # chain all of these dependencies + } + if ( $self->export_username($old) ne $self->export_username($new) ) { my $usergroup = $self->option('usergroup') || 'usergroup'; my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'rename', @@ -192,6 +218,13 @@ sub _export_replace { $dbh->rollback if $oldAutoCommit; return $err_or_queue; } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } $jobnum = $err_or_queue->jobnum; } @@ -274,6 +307,23 @@ sub _export_suspend { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + my $jobnum = ''; + + # disconnect users before changing anything + if ($self->option('disconnect_ssh')) { + my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', + 'disconnect_ssh' => $self->option('disconnect_ssh'), + 'svc_acct_username' => $svc_acct->username, + 'disconnect_port' => $self->option('disconnect_port'), + 'disconnect_log' => $self->option('disconnect_log'), + ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + $jobnum = $err_or_queue->jobnum; + } + my @newgroups = $self->suspended_usergroups($svc_acct); unless (@newgroups) { #don't change password if assigning to a suspended group @@ -284,7 +334,13 @@ sub _export_suspend { $dbh->rollback if $oldAutoCommit; return $err_or_queue; } - + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } } my $error = @@ -345,9 +401,29 @@ sub _export_unsuspend { sub _export_delete { my( $self, $svc_x ) = (shift, shift); + + my $jobnum = ''; + + # disconnect users before changing anything + if ($self->option('disconnect_ssh')) { + my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'user_disconnect', + 'disconnect_ssh' => $self->option('disconnect_ssh'), + 'svc_acct_username' => $svc_x->username, + 'disconnect_port' => $self->option('disconnect_port'), + 'disconnect_log' => $self->option('disconnect_log'), + ); + return $err_or_queue unless ref($err_or_queue); + $jobnum = $err_or_queue->jobnum; + } + my $usergroup = $self->option('usergroup') || 'usergroup'; my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'delete', $self->export_username($svc_x), $usergroup ); + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + return $error if $error; + } + ref($err_or_queue) ? '' : $err_or_queue; } @@ -1164,6 +1240,53 @@ sub sqlradius_group_replace { or die $dbh->errstr; } +=item sqlradius_user_disconnect + +For a specified user, sends a disconnect request to all nas in the server database. + +Accepts L connection input and the following named parameters: + +I - user@host with access to radclient program (required) + +I - the user to be disconnected (required) + +I - the port (on the nas) to send disconnect requests to (defaults to 1700) + +I - if true, print disconnect command & output to the error log + +Note this is NOT the opposite of sqlradius_connect. + +=cut + +sub sqlradius_user_disconnect { + my $dbh = sqlradius_connect(shift, shift, shift); + my %opt = @_; + # get list of nas + my $sth = $dbh->prepare('select nasname, secret from nas') or die $dbh->errstr; + $sth->execute() or die $dbh->errstr; + my $nas = $sth->fetchall_arrayref({}); + $sth->finish(); + $dbh->disconnect(); + die "No nas found in radius db" unless @$nas; + # set up ssh connection + eval "use Net::SSH"; + my $ssh = Net::OpenSSH->new($opt{'disconnect_ssh'}); + die "Couldn't establish SSH connection: " . $ssh->error + if $ssh->error; + # send individual disconnect requests + my $user = $opt{'svc_acct_username'}; #svc_acct username + my $port = $opt{'disconnect_port'} || 1700; #or should we pull this from the db? + foreach my $nas (@$nas) { + my $nasname = $nas->{'nasname'}; + my $secret = $nas->{'secret'}; + my $command = qq(echo "User-Name=$user" | radclient -r 1 $nasname:$port disconnect '$secret'); + my ($output, $errput) = $ssh->capture2($command); + warn $command . "\n" . $output . $errput . $ssh->error . "\n" + if $opt{'disconnect_log'}; + } + return ''; +} + ### # class method to fetch groups/attributes from the sqlradius install on upgrade ### -- cgit v1.2.1 From 80d97e2e53aaab8ac67da8e43c6352222b78c1d5 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 22 Jul 2015 16:47:50 -0500 Subject: RT#37165: Print document when account is created [added svcnum to job] --- FS/FS/part_export/print_template.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'FS') diff --git a/FS/FS/part_export/print_template.pm b/FS/FS/part_export/print_template.pm index 21f0a152e..65e68ac56 100644 --- a/FS/FS/part_export/print_template.pm +++ b/FS/FS/part_export/print_template.pm @@ -142,7 +142,8 @@ sub print_template { my ($self, $phase, $svc_x) = @_; if ($self->option('phase') eq $phase) { my $queue = new FS::queue { - 'job' => 'FS::part_export::print_template::process_print_template', + 'svcnum' => $svc_x->svcnum, + 'job' => 'FS::part_export::print_template::process_print_template', }; my $error = $queue->insert( 'svcnum' => $svc_x->svcnum, -- cgit v1.2.1 From 68d0a78fcbdaac34656af91640886cce5dd6e501 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Wed, 22 Jul 2015 18:33:20 -0500 Subject: RT#37165: Print document when account is created [fixed svc list] --- FS/FS/part_export/print_template.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_export/print_template.pm b/FS/FS/part_export/print_template.pm index 65e68ac56..b299ab493 100644 --- a/FS/FS/part_export/print_template.pm +++ b/FS/FS/part_export/print_template.pm @@ -38,9 +38,9 @@ tie my %options, 'Tie::IxHash', %info = ( #unfortunately, FS::part_svc->svc_tables fails at this point, not sure why - 'svc' => [ map { 'svc_'.$_ } qw( - acct domain cert forward mailinglist www broadband cable dsl - conferencing video dish hardware phone pbx circuit port alarm external) + 'svc' => [ qw( svc_acct svc_domain svc_cert svc_forward svc_mailinglist svc_www + svc_broadband svc_cable svc_dsl svc_conferencing svc_video svc_dish + svc_hardware svc_phone svc_pbx svc_circuit svc_port svc_alarm svc_external ) ], 'desc' => 'Print document during service change, for all services', 'options' => \%options, -- cgit v1.2.1 From 10f2ba3c0fa4c1147be6535ef10890d2f2defe9f Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 23 Jul 2015 01:43:40 -0500 Subject: RT#37163 Disconnect Users via Radclient [die on error] --- FS/FS/part_export/sqlradius.pm | 141 ++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 65 deletions(-) (limited to 'FS') diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm index 1f5b018ce..048a24485 100644 --- a/FS/FS/part_export/sqlradius.pm +++ b/FS/FS/part_export/sqlradius.pm @@ -10,6 +10,7 @@ use FS::svc_acct; use FS::export_svc; use Carp qw( cluck ); use NEXT; +use Net::OpenSSH; @ISA = qw(FS::part_export); @EXPORT_OK = qw( sqlradius_connect ); @@ -79,8 +80,8 @@ tie %options, 'Tie::IxHash', 'disconnect_port' => { label => 'Port to send disconnection requests to, default 1700', }, - 'disconnect_log' => { - label => 'Print disconnect output and errors to the queue log (will otherwise fail silently)', + 'disconnect_ignore_error' => { + label => 'Ignore disconnection request errors', type => 'checkbox', }, ; @@ -194,22 +195,6 @@ sub _export_replace { my $dbh = dbh; my $jobnum = ''; - - # disconnect users before changing username - if ($self->option('disconnect_ssh')) { - my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', - 'disconnect_ssh' => $self->option('disconnect_ssh'), - 'svc_acct_username' => $old->username, - 'disconnect_port' => $self->option('disconnect_port'), - 'disconnect_log' => $self->option('disconnect_log'), - ); - unless ( ref($err_or_queue) ) { - $dbh->rollback if $oldAutoCommit; - return $err_or_queue; - } - $jobnum = $err_or_queue->jobnum; # chain all of these dependencies - } - if ( $self->export_username($old) ne $self->export_username($new) ) { my $usergroup = $self->option('usergroup') || 'usergroup'; my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'rename', @@ -218,13 +203,6 @@ sub _export_replace { $dbh->rollback if $oldAutoCommit; return $err_or_queue; } - if ( $jobnum ) { - my $error = $err_or_queue->depend_insert( $jobnum ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } $jobnum = $err_or_queue->jobnum; } @@ -274,7 +252,7 @@ sub _export_replace { my $error; my (@oldgroups) = $old->radius_groups('hashref'); my (@newgroups) = $new->radius_groups('hashref'); - $error = $self->sqlreplace_usergroups( $new->svcnum, + ($error,$jobnum) = $self->sqlreplace_usergroups( $new->svcnum, $self->export_username($new), $jobnum ? $jobnum : '', \@oldgroups, @@ -285,6 +263,28 @@ sub _export_replace { return $error; } + # radius database is used for authorization, so to avoid users reauthorizing + # before the database changes, disconnect users after changing database + if ($self->option('disconnect_ssh')) { + my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', + 'disconnect_ssh' => $self->option('disconnect_ssh'), + 'svc_acct_username' => $old->username, + 'disconnect_port' => $self->option('disconnect_port'), + 'ignore_error' => $self->option('disconnect_ignore_error'), + ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -309,21 +309,6 @@ sub _export_suspend { my $jobnum = ''; - # disconnect users before changing anything - if ($self->option('disconnect_ssh')) { - my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', - 'disconnect_ssh' => $self->option('disconnect_ssh'), - 'svc_acct_username' => $svc_acct->username, - 'disconnect_port' => $self->option('disconnect_port'), - 'disconnect_log' => $self->option('disconnect_log'), - ); - unless ( ref($err_or_queue) ) { - $dbh->rollback if $oldAutoCommit; - return $err_or_queue; - } - $jobnum = $err_or_queue->jobnum; - } - my @newgroups = $self->suspended_usergroups($svc_acct); unless (@newgroups) { #don't change password if assigning to a suspended group @@ -334,16 +319,11 @@ sub _export_suspend { $dbh->rollback if $oldAutoCommit; return $err_or_queue; } - if ( $jobnum ) { - my $error = $err_or_queue->depend_insert( $jobnum ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } + $jobnum = $err_or_queue->jobnum; } - my $error = + my $error; + ($error,$jobnum) = $self->sqlreplace_usergroups( $new->svcnum, $self->export_username($new), @@ -355,6 +335,28 @@ sub _export_suspend { $dbh->rollback if $oldAutoCommit; return $error; } + + # radius database is used for authorization, so to avoid users reauthorizing + # before the database changes, disconnect users after changing database + if ($self->option('disconnect_ssh')) { + my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'user_disconnect', + 'disconnect_ssh' => $self->option('disconnect_ssh'), + 'svc_acct_username' => $svc_acct->username, + 'disconnect_port' => $self->option('disconnect_port'), + ); + unless ( ref($err_or_queue) ) { + $dbh->rollback if $oldAutoCommit; + return $err_or_queue; + } + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -404,24 +406,25 @@ sub _export_delete { my $jobnum = ''; - # disconnect users before changing anything + my $usergroup = $self->option('usergroup') || 'usergroup'; + my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'delete', + $self->export_username($svc_x), $usergroup ); + $jobnum = $err_or_queue->jobnum; + + # radius database is used for authorization, so to avoid users reauthorizing + # before the database changes, disconnect users after changing database if ($self->option('disconnect_ssh')) { my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'user_disconnect', 'disconnect_ssh' => $self->option('disconnect_ssh'), 'svc_acct_username' => $svc_x->username, 'disconnect_port' => $self->option('disconnect_port'), - 'disconnect_log' => $self->option('disconnect_log'), + 'ignore_error' => $self->option('disconnect_ignore_error'), ); return $err_or_queue unless ref($err_or_queue); - $jobnum = $err_or_queue->jobnum; - } - - my $usergroup = $self->option('usergroup') || 'usergroup'; - my $err_or_queue = $self->sqlradius_queue( $svc_x->svcnum, 'delete', - $self->export_username($svc_x), $usergroup ); - if ( $jobnum ) { - my $error = $err_or_queue->depend_insert( $jobnum ); - return $error if $error; + if ( $jobnum ) { + my $error = $err_or_queue->depend_insert( $jobnum ); + return $error if $error; + } } ref($err_or_queue) ? '' : $err_or_queue; @@ -616,6 +619,8 @@ sub sqlradius_connect { DBI->connect(@_) or die $DBI::errstr; } +# on success, returns '' in scalar context, ('',$jobnum) in list context +# on error, always just returns error sub sqlreplace_usergroups { my ($self, $svcnum, $username, $jobnum, $old, $new) = @_; @@ -657,8 +662,9 @@ sub sqlreplace_usergroups { my $error = $err_or_queue->depend_insert( $jobnum ); return $error if $error; } + $jobnum = $err_or_queue->jobnum; # chain all of these dependencies } - ''; + wantarray ? ('',$jobnum) : ''; } @@ -1252,7 +1258,7 @@ I - the user to be disconnected (required) I - the port (on the nas) to send disconnect requests to (defaults to 1700) -I - if true, print disconnect command & output to the error log +I - do not die on error with the disconnect request Note this is NOT the opposite of sqlradius_connect. @@ -1269,21 +1275,26 @@ sub sqlradius_user_disconnect { $dbh->disconnect(); die "No nas found in radius db" unless @$nas; # set up ssh connection - eval "use Net::SSH"; my $ssh = Net::OpenSSH->new($opt{'disconnect_ssh'}); die "Couldn't establish SSH connection: " . $ssh->error if $ssh->error; # send individual disconnect requests my $user = $opt{'svc_acct_username'}; #svc_acct username my $port = $opt{'disconnect_port'} || 1700; #or should we pull this from the db? + my $error = ''; foreach my $nas (@$nas) { my $nasname = $nas->{'nasname'}; my $secret = $nas->{'secret'}; my $command = qq(echo "User-Name=$user" | radclient -r 1 $nasname:$port disconnect '$secret'); my ($output, $errput) = $ssh->capture2($command); - warn $command . "\n" . $output . $errput . $ssh->error . "\n" - if $opt{'disconnect_log'}; + $error .= "Error running $command: $errput " . $ssh->error . " " + if $errput || $ssh->error; } + $error .= "Some clients may have successfully disconnected" + if $error && (@$nas > 1); + $error = "No clients found" + unless @$nas; + die $error if $error && !$opt{'ignore_error'}; return ''; } -- cgit v1.2.1 From 1add633372bdca3cc7163c2ce48363fed3984437 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Fri, 24 Jul 2015 18:19:56 -0700 Subject: automate RBC payment batch transfer, #35228 --- FS/FS/Conf.pm | 7 ++ FS/FS/pay_batch.pm | 58 ++++++++++------ FS/bin/freeside-rbc-download | 160 +++++++++++++++++++++++++++++++++++++++++++ FS/bin/freeside-rbc-upload | 115 +++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+), 21 deletions(-) create mode 100755 FS/bin/freeside-rbc-download create mode 100755 FS/bin/freeside-rbc-upload (limited to 'FS') diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 4e1736be3..c93608266 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3855,6 +3855,13 @@ and customer address. Include units.', 'type' => 'textarea', }, + { + 'key' => 'batchconfig-RBC-login', + 'section' => 'billing', + 'description' => 'FTPS login for uploading Royal Bank of Canada batches. Two lines: 1. username, 2. password. If not supplied, batches can still be created but not automatically uploaded.', + 'type' => 'textarea', + }, + { 'key' => 'batchconfig-td_eft1464', 'section' => 'billing', diff --git a/FS/FS/pay_batch.pm b/FS/FS/pay_batch.pm index a7628f6e0..df969a00f 100644 --- a/FS/FS/pay_batch.pm +++ b/FS/FS/pay_batch.pm @@ -209,7 +209,9 @@ foreach my $INC (@INC) { =item import_results OPTION => VALUE, ... -Import batch results. +Import batch results. Can be called as an instance method, if you want to +automatically adjust status on a specific batch, or a class method, if you +don't know which batch(es) the results apply to. Options are: @@ -280,6 +282,8 @@ sub import_results { my $declined_condition = $info->{'declined'}; my $close_condition = $info->{'close_condition'}; + my %target_batches; # batches that had at least one payment updated + my $csv = new Text::CSV_XS; local $SIG{HUP} = 'IGNORE'; @@ -293,13 +297,17 @@ sub import_results { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $reself = $self->select_for_update; + if ( ref($self) ) { + # if called on a specific pay_batch, check the status of that batch + # before continuing + my $reself = $self->select_for_update; - if ( $reself->status ne 'I' - and !$conf->exists('batch-manual_approval') ) { - $dbh->rollback if $oldAutoCommit; - return "batchnum ". $self->batchnum. "no longer in transit"; - } + if ( $reself->status ne 'I' + and !$conf->exists('batch-manual_approval') ) { + $dbh->rollback if $oldAutoCommit; + return "batchnum ". $self->batchnum. "no longer in transit"; + } + } # otherwise we can't enforce this constraint. sorry. my $total = 0; my $line; @@ -345,6 +353,7 @@ sub import_results { push @all_values, \@values; } elsif ($filetype eq 'variable') { + # no longer used my @values = ( eval { $parse->($self, $line) } ); if( $@ ) { $dbh->rollback if $oldAutoCommit; @@ -404,6 +413,9 @@ sub import_results { unless ( $cust_pay_batch ) { return "unknown paybatchnum $hash{'paybatchnum'}\n"; } + # remember that we've touched this batch + $target_batches{ $cust_pay_batch->batchnum } = 1; + my $custnum = $cust_pay_batch->custnum, my $payby = $cust_pay_batch->payby, @@ -443,21 +455,25 @@ sub import_results { } # foreach (@all_values) - my $close = 1; - if ( defined($close_condition) ) { - # Allow the module to decide whether to close the batch. - # $close_condition can also die() to abort the whole import. - $close = eval { $close_condition->($self) }; - if ( $@ ) { - $dbh->rollback; - die $@; + # decide whether to close batches that had payments posted + foreach my $batchnum (keys %target_batches) { + my $pay_batch = FS::pay_batch->by_key($batchnum); + my $close = 1; + if ( defined($close_condition) ) { + # Allow the module to decide whether to close the batch. + # $close_condition can also die() to abort the whole import. + $close = eval { $close_condition->($pay_batch) }; + if ( $@ ) { + $dbh->rollback; + die $@; + } } - } - if ( $close ) { - my $error = $self->set_status('R'); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + if ( $close ) { + my $error = $pay_batch->set_status('R'); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } } diff --git a/FS/bin/freeside-rbc-download b/FS/bin/freeside-rbc-download new file mode 100755 index 000000000..376b839e1 --- /dev/null +++ b/FS/bin/freeside-rbc-download @@ -0,0 +1,160 @@ +#!/usr/bin/perl + +use strict; +use Getopt::Std; +use Date::Format qw(time2str); +use File::Temp qw(tempdir); #0.19 for ->newdir() interface, not in 5.10.0 +use Net::FTPSSL; +use FS::UID qw(adminsuidsetup dbh); +use FS::Record qw(qsearch qsearchs); +use FS::pay_batch; +use FS::Conf; + +use vars qw( $opt_v $opt_a $opt_f ); +getopts('va:f:'); + +#$Net::SFTP::Foreign::debug = -1; +sub usage { " + Usage: + freeside-rbc-download [ -v ] [ -a archivedir ] [ -f filename ] user\n +" } + +sub debug { + print STDERR $_[0] if $opt_v; +} + +my $user = shift or die &usage; +adminsuidsetup $user; + +$FS::UID::AutoCommit = 0; +my $dbh = dbh; + +if ( $opt_a ) { + die "no such directory: $opt_a\n" + unless -d $opt_a; + die "archive directory $opt_a is not writable by the freeside user\n" + unless -w $opt_a; +} + +my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere? + +my $conf = new FS::Conf; +my ($username, $password) = $conf->config('batchconfig-RBC-login'); +$username and $password + or die "RBC FTP login not configured. Enter your username and password in 'batchconfig-rbc-login'.\n"; + +my $host = 'ftpssl.rbc.com'; +debug "Connecting to $username\@$host...\n"; + +my $ftp = Net::FTPSSL->new($host, + Timeout => 30, + Debug => ($opt_v ? 1 : 0), + Croak => 1, # rely on auto-rollback when dbh closes + ); +$ftp->login($username, $password); + +# directory layout: +# ~/ # upload to here +# ~/inbound +# ~/inbound/valid # batches move here while being processed +# ~/outbound +# ~/outbound/XXXX # string of four characters; results arrive here + +$ftp->cwd('outbound'); +for my $dir ( $ftp->nlst ) { + debug "Entering outbound/$dir\n"; + $ftp->cwd($dir); + FILE: for my $filename ( $ftp->nlst ) { + debug "$filename..."; + # filenames look like "RPT9999X.111". + # 9999 is the four-digit report type + # X is "P" for production or "T" for test + # 111 is the sequential file number + if ( $opt_f ) { + if ( $filename ne $opt_f ) { + debug "is not the requested file.\n"; + next FILE; + } + # -f can be used to download/process any file, even one that doesn't fit + # the naming rule (e.g. those that are already downloaded). + } elsif ( $filename =~ /^RPT(\d{4})[PT]\.\d{3}$/ ) { + # fallthrough; don't currently reject files based on RPT type, because + # our parser should be able to figure it out + } else { + debug "skipped.\n"; + next FILE; + } + + debug "downloading.\n"; + $ftp->get($filename, "$tmpdir/$filename"); + + #copy to archive dir + if ( $opt_a ) { + debug "Copying to archive dir $opt_a\n"; + system 'cp', "$tmpdir/$filename", $opt_a; + warn "failed to copy $tmpdir/$filename to $opt_a: $!\n" if $!; + } + + debug "Processing batch..."; + open(my $fh, '<', "$tmpdir/$filename") + or die "couldn't read temp file: $!\n"; + + my $error = FS::pay_batch->import_results( + filehandle => $fh, + format => 'RBC', + ); + + if ( $error ) { + die "Processing $filename failed:\n$error\n\n"; + } + + debug "done.\n"; + } # FILE + $ftp->cdup(); +} # $dir + +debug "Finished.\n"; +dbh->commit; +exit(0); + +=head1 NAME + +freeside-rbc-download - Retrieve payment batch responses from RBC. + +=head1 SYNOPSIS + + freeside-rbc-download [ -v ] [ -f filename ] [ -a archivedir ] user + +=head1 DESCRIPTION + +Command line tool to download payment batch responses from the Royal Bank of +Canada ACH service. These files are fixed-width data files containing some +combination of valid, returned, or reversed payment records. + +By default, the script will download any files with names like "RPT9999X.111" +where 9999 is a four-digit document type code (like "0900", all records), X is +the letter "P" for production or "T" for test mode, and 111 is a counter +incremented with each new response file. After the files are downloaded, RBC's +server will automatically rename them with the suffix '.downloaded%FTPS' to +avoid double-processing them. + + +-v: Be verbose. + +-f filename: Download a file with a specific name, instead of all files +matching the pattern. This can be used to reprocess a specific file. + +-a directory: Archive the files in the specified directory. + +user: freeside username + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/bin/freeside-rbc-upload b/FS/bin/freeside-rbc-upload new file mode 100755 index 000000000..52501028c --- /dev/null +++ b/FS/bin/freeside-rbc-upload @@ -0,0 +1,115 @@ +#!/usr/bin/perl + +use strict; +use Getopt::Std; +use DateTime; +use Net::FTPSSL; +use File::Temp qw(tempdir); +use File::Slurp 'write_file'; +use FS::UID qw(adminsuidsetup dbh); +use FS::Record qw(qsearch qsearchs); +use FS::pay_batch; +use FS::Conf; + +use vars qw( $opt_a $opt_v $opt_p ); +getopts('avp:'); + +sub usage { " + Usage: + freeside-rbc-upload [ -v ] user batchnum + freeside-rbc-upload -a [ -p payby ] [ -v ] user\n +" } + +sub debug { + print STDERR $_[0] if $opt_v; +} + +my $user = shift or die &usage; +adminsuidsetup $user; + +my @batches; + +# copied from freeside-paymentech-upload, obviously +if($opt_a) { + my %criteria = (status => 'O'); + $criteria{'payby'} = uc($opt_p) if $opt_p; + @batches = qsearch('pay_batch', \%criteria); + die "No open batches found".($opt_p ? " of type '$opt_p'" : '').".\n" + if !@batches; +} +else { + my $batchnum = shift; + die &usage if !$batchnum; + @batches = qsearchs('pay_batch', { batchnum => $batchnum } ); + die "Can't find payment batch '$batchnum'\n" if !@batches; +} + +my $conf = new FS::Conf; +my ($username, $password) = $conf->config('batchconfig-RBC-login'); + +$username and $password + or die "RBC FTP login not configured. Enter your username and password in 'batchconfig-rbc-login'.\n"; + +my $host = 'ftpssl.rbc.com'; +debug "Connecting to $username\@$host...\n"; + +my $date = DateTime->now->strftime('%Y%m%d'); + +my $ftp = Net::FTPSSL->new($host, + Timeout => 30, + Debug => ($opt_v ? 1 : 0), + Croak => 1, # rely on auto-rollback when dbh closes + ); +$ftp->login($username, $password); + +my $tmpdir = tempdir( CLEANUP => 1 ); + +foreach my $pay_batch (@batches) { + my $batchnum = $pay_batch->batchnum; + my $filename = $date . '.' . sprintf('%06d', $batchnum); + debug "Exporting batch $batchnum to $filename\n"; + + my $text = $pay_batch->export_batch(format => 'RBC'); + write_file("$tmpdir/$filename", $text); + + debug "Uploading $filename..."; + $ftp->put("$tmpdir/$filename", $filename); + debug "done.\n"; +} + +debug "Finished.\n"; + +=head1 NAME + +freeside-rbc-upload - Transmit a payment batch to RBC via FTP/TLS. + +=head1 SYNOPSIS + + freeside-rbc-upload [ -a [ -p PAYBY ] ] [ -v ] user batchnum + +=head1 DESCRIPTION + +Command line tool to upload a payment batch to the Royal Bank of Canada +ACH service. Use L to retrieve the response file. +Options: + +-a: Send all open batches, instead of specifying a batchnum. + +-p PAYBY: With -a, limit to batches of that payment type, e.g. -p CARD. + +-v: Be verbose. + +user: freeside username + +batchnum: pay_batch primary key + +=head1 BUGS + +=head1 SEE ALSO + +L + +=cut + +1; + -- cgit v1.2.1