From: Ivan Kohler Date: Thu, 4 Aug 2016 00:52:34 +0000 (-0700) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=cf54023e010df76e0c39ac70902877d7c4c94c6e;hp=d7759b49c2ff3b220ab328767645bfed85d18f31 Merge branch 'master' of git.freeside.biz:/home/git/freeside --- diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index d767e910d..685821bad 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -1627,6 +1627,34 @@ sub insert_payby { } +sub update_payby { + my $p = shift; + + my($context, $session, $custnum) = _custoragent_session_custnum($p); + return { 'error' => $session } if $context eq 'error'; + + my $cust_payby = qsearchs('cust_payby', { + 'custnum' => $custnum, + 'custpaybynum' => $p->{'custpaybynum'}, + }) + or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} }; + + foreach my $field ( + qw( weight payby payinfo paycvv paydate payname paystate paytype payip ) + ) { + next unless exists($p->{$field}); + $cust_payby->set($field,$p->{$field}); + } + + my $error = $cust_payby->replace; + if ( $error ) { + return { 'error' => $error }; + } else { + return { 'custpaybynum' => $cust_payby->custpaybynum }; + } + +} + sub verify_payby { my $p = shift; diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm index 622f3df05..08c6c2d59 100644 --- a/FS/FS/ClientAPI_XMLRPC.pm +++ b/FS/FS/ClientAPI_XMLRPC.pm @@ -129,6 +129,7 @@ sub ss2clientapi { 'list_invoices' => 'MyAccount/list_invoices', #? 'list_payby' => 'MyAccount/list_payby', 'insert_payby' => 'MyAccount/insert_payby', + 'update_payby' => 'MyAccount/update_payby', 'delete_payby' => 'MyAccount/delete_payby', 'cancel' => 'MyAccount/cancel', #add to ss cgi! 'payment_info' => 'MyAccount/payment_info', diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 94b8839a1..1b50006a5 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -4950,6 +4950,13 @@ and customer address. Include units.', # }, { + 'key' => 'cdr-skip_duplicate_rewrite', + 'section' => 'telephony', + 'description' => 'Use the freeside-cdrrewrited daemon to prevent billing CDRs with a src, dst and calldate identical to an existing CDR', + 'type' => 'checkbox', + }, + + { 'key' => 'cdr-charged_party_rewrite', 'section' => 'telephony', 'description' => 'Do charged party rewriting in the freeside-cdrrewrited daemon; useful if CDRs are being dropped off directly in the database and require special charged_party processing such as cdr-charged_party-accountcode or cdr-charged_party-truncate*.', diff --git a/FS/FS/Log.pm b/FS/FS/Log.pm index 2fd002093..aed1f3969 100644 --- a/FS/FS/Log.pm +++ b/FS/FS/Log.pm @@ -5,13 +5,20 @@ use FS::Record qw(qsearch qsearchs); use FS::Conf; use FS::Log::Output; use FS::log; -use vars qw(@STACK @LEVELS); +use vars qw(@STACK %LEVELS); # override the stringification of @_ with something more sensible. BEGIN { - @LEVELS = qw(debug info notice warning error critical alert emergency); + # subset of Log::Dispatch levels + %LEVELS = ( + 0 => 'debug', + 1 => 'info', + 3 => 'warning', + 4 => 'error', + 5 => 'critical' + ); - foreach my $l (@LEVELS) { + foreach my $l (values %LEVELS) { my $sub = sub { my $self = shift; $self->log( level => $l, message => @_ ); @@ -100,4 +107,24 @@ sub DESTROY { splice(@STACK, $self->{'index'}, 1); # delete the stack entry } +=item levelnums + +Subroutine. Returns ordered list of level nums. + +=cut + +sub levelnums { + sort keys %LEVELS; +} + +=item levelmap + +Subroutine. Returns ordered map of level num => level name. + +=cut + +sub levelmap { + map { $_ => $LEVELS{$_} } levelnums; +} + 1; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 1008fd5d8..245bdea88 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -413,6 +413,8 @@ if ( -e $addl_handler_use_file ) { use FS::olt_site; use FS::access_user_page_pref; use FS::part_svc_msgcat; + use FS::commission_schedule; + use FS::commission_rate; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 7f76d9988..c3d397389 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -2,6 +2,7 @@ package FS::Record; use base qw( Exporter ); use strict; +use charnames ':full'; use vars qw( $AUTOLOAD %virtual_fields_cache %fk_method_cache $fk_table_cache $money_char $lat_lower $lon_upper @@ -2913,6 +2914,10 @@ sub ut_coord { my $coord = $self->getfield($field); my $neg = $coord =~ s/^(-)//; + # ignore degree symbol at the end, + # but not otherwise supporting degree/minutes/seconds symbols + $coord =~ s/\N{DEGREE SIGN}\s*$//; + my ($d, $m, $s) = (0, 0, 0); if ( @@ -3220,6 +3225,22 @@ sub ut_agentnum_acl { } +=item trim_whitespace FIELD[, FIELD ... ] + +Strip leading and trailing spaces from the value in the named FIELD(s). + +=cut + +sub trim_whitespace { + my $self = shift; + foreach my $field (@_) { + my $value = $self->get($field); + $value =~ s/^\s+//; + $value =~ s/\s+$//; + $self->set($field, $value); + } +} + =item fields [ TABLE ] This is a wrapper for real_fields. Code that called diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index ac585108e..8661c4b97 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1361,6 +1361,7 @@ sub tables_hashref { 'commission_agentnum', 'int', 'NULL', '', '', '', # 'commission_salesnum', 'int', 'NULL', '', '', '', # 'commission_pkgnum', 'int', 'NULL', '', '', '', # + 'commission_invnum', 'int', 'NULL', '', '', '', 'credbatch', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'crednum', @@ -1396,6 +1397,10 @@ sub tables_hashref { table => 'cust_pkg', references => [ 'pkgnum' ], }, + { columns => [ 'commission_invnum' ], + table => 'cust_bill', + references => [ 'invnum' ], + }, ], }, @@ -1417,6 +1422,7 @@ sub tables_hashref { 'commission_agentnum', 'int', 'NULL', '', '', '', 'commission_salesnum', 'int', 'NULL', '', '', '', 'commission_pkgnum', 'int', 'NULL', '', '', '', + 'commission_invnum', 'int', 'NULL', '', '', '', #void fields 'void_date', @date_type, '', '', 'void_reason', 'varchar', 'NULL', $char_d, '', '', @@ -1456,6 +1462,10 @@ sub tables_hashref { table => 'cust_pkg', references => [ 'pkgnum' ], }, + { columns => [ 'commission_invnum' ], + table => 'cust_bill', + references => [ 'invnum' ], + }, { columns => [ 'void_reasonnum' ], table => 'reason', references => [ 'reasonnum' ], @@ -7438,6 +7448,36 @@ sub tables_hashref { ], }, + 'commission_schedule' => { + 'columns' => [ + 'schedulenum', 'serial', '', '', '', '', + 'schedulename', 'varchar', '', $char_d, '', '', + 'reasonnum', 'int', 'NULL', '', '', '', + 'basis', 'varchar', 'NULL', 32, '', '', + ], + 'primary_key' => 'schedulenum', + 'unique' => [], + 'index' => [], + }, + + 'commission_rate' => { + 'columns' => [ + 'commissionratenum', 'serial', '', '', '', '', + 'schedulenum', 'int', '', '', '', '', + 'cycle', 'int', '', '', '', '', + 'amount', @money_type, '', '', + 'percent', 'decimal','', '7,4', '', '', + ], + 'primary_key' => 'commissionratenum', + 'unique' => [ [ 'schedulenum', 'cycle', ] ], + 'index' => [], + 'foreign_keys' => [ + { columns => [ 'schedulenum' ], + table => 'commission_schedule', + }, + ], + }, + # name type nullability length default local #'new_table' => { diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm index db7010c18..3e3e7e520 100644 --- a/FS/FS/TaxEngine/internal.pm +++ b/FS/FS/TaxEngine/internal.pm @@ -28,8 +28,10 @@ sub add_sale { push @{ $self->{items} }, $cust_bill_pkg; - my @loc_keys = qw( district city county state country ); - my %taxhash = map { $_ => $location->get($_) } @loc_keys; + my %taxhash = map { $_ => $location->get($_) } + qw( district county state country ); + # city names in cust_main_county are uppercase + $taxhash{'city'} = uc($location->get('city')); $taxhash{'taxclass'} = $part_item->taxclass; diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 6f14cd202..3faf47e24 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -352,6 +352,9 @@ sub upgrade_data { tie my %hash, 'Tie::IxHash', + #remap log levels + 'log' => [], + #cust_main (remove paycvv from history, locations, cust_payby, etc) 'cust_main' => [], @@ -478,8 +481,12 @@ sub upgrade_data { #populate tax statuses 'tax_status' => [], - #mark certain taxes as system-maintained + #mark certain taxes as system-maintained, + # and fix whitespace 'cust_main_county' => [], + + #fix whitespace + 'cust_location' => [], ; \%hash; diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm index 0ee0aa04a..13a826f29 100644 --- a/FS/FS/access_right.pm +++ b/FS/FS/access_right.pm @@ -253,7 +253,9 @@ sub _upgrade_data { # class method 'Generate quotation' => 'Disable quotation', 'Add on-the-fly void credit reason' => 'Add on-the-fly void reason', '_ALL' => 'Employee preference telephony integration', - 'Edit customer package dates' => 'Change package start date', #4.x + 'Edit customer package dates' => [ 'Change package start date', #4.x + 'Change package contract end date', + ], 'Resend invoices' => 'Print and mail invoices', ); diff --git a/FS/FS/commission_rate.pm b/FS/FS/commission_rate.pm new file mode 100644 index 000000000..dcb596d60 --- /dev/null +++ b/FS/FS/commission_rate.pm @@ -0,0 +1,116 @@ +package FS::commission_rate; +use base qw( FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::commission_rate - Object methods for commission_rate records + +=head1 SYNOPSIS + + use FS::commission_rate; + + $record = new FS::commission_rate \%hash; + $record = new FS::commission_rate { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::commission_rate object represents a commission rate (a percentage or a +flat amount) that will be paid on a customer's N-th invoice. The sequence of +commissions that will be paid on consecutive invoices is the parent object, +L. + +FS::commission_rate inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item commissionratenum - primary key + +=item schedulenum - L foreign key + +=item cycle - the ordinal of the billing cycle this commission will apply +to. cycle = 1 applies to the customer's first invoice, cycle = 2 to the +second, etc. + +=item amount - the flat amount to pay per invoice in commission + +=item percent - the percentage of the invoice amount to pay in +commission + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new commission rate. To add it to the database, see L<"insert">. + +=cut + +sub table { 'commission_rate'; } + +=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 commission rate. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + $self->set('amount', '0.00') + if $self->get('amount') eq ''; + $self->set('percent', '0') + if $self->get('percent') eq ''; + + my $error = + $self->ut_numbern('commissionratenum') + || $self->ut_number('schedulenum') + || $self->ut_number('cycle') + || $self->ut_money('amount') + || $self->ut_decimal('percent') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 SEE ALSO + +L + +=cut + +1; + diff --git a/FS/FS/commission_schedule.pm b/FS/FS/commission_schedule.pm new file mode 100644 index 000000000..375386c33 --- /dev/null +++ b/FS/FS/commission_schedule.pm @@ -0,0 +1,235 @@ +package FS::commission_schedule; +use base qw( FS::o2m_Common FS::Record ); + +use strict; +use FS::Record qw( qsearch qsearchs ); +use FS::commission_rate; +use Tie::IxHash; + +tie our %basis_options, 'Tie::IxHash', ( + setuprecur => 'Total sales', + setup => 'One-time and setup charges', + recur => 'Recurring charges', + setup_cost => 'Setup costs', + recur_cost => 'Recurring costs', + setup_margin => 'Setup charges minus costs', + recur_margin_permonth => 'Monthly recurring charges minus costs', +); + +=head1 NAME + +FS::commission_schedule - Object methods for commission_schedule records + +=head1 SYNOPSIS + + use FS::commission_schedule; + + $record = new FS::commission_schedule \%hash; + $record = new FS::commission_schedule { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::commission_schedule object represents a bundle of one or more +commission rates for invoices. FS::commission_schedule inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item schedulenum - primary key + +=item schedulename - descriptive name + +=item reasonnum - the credit reason (L) that will be assigned +to these commission credits + +=item basis - for percentage credits, which component of the invoice charges +the percentage will be calculated on: +- setuprecur (total charges) +- setup +- recur +- setup_cost +- recur_cost +- setup_margin (setup - setup_cost) +- recur_margin_permonth ((recur - recur_cost) / freq) + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new commission schedule. To add the object to the database, see +L<"insert">. + +=cut + +sub table { 'commission_schedule'; } + +=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. + +=cut + +sub delete { + my $self = shift; + # don't allow the schedule to be removed if it's still linked to events + if ($self->part_event) { + return 'This schedule is still in use.'; # UI should be smarter + } + $self->process_o2m( + 'table' => 'commission_rate', + 'params' => [], + ) || $self->delete; +} + +=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 record. 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('schedulenum') + || $self->ut_text('schedulename') + || $self->ut_number('reasonnum') + || $self->ut_enum('basis', [ keys %basis_options ]) + ; + return $error if $error; + + $self->SUPER::check; +} + +=item part_event + +Returns a list of billing events (L objects) that pay +commission on this schedule. + +=cut + +sub part_event { + my $self = shift; + map { $_->part_event } + qsearch('part_event_option', { + optionname => 'schedulenum', + optionvalue => $self->schedulenum, + } + ); +} + +=item calc_credit INVOICE + +Takes an L object and calculates credit on this schedule. +Returns the amount to credit. If there's no rate defined for this invoice, +returns nothing. + +=cut + +# Some false laziness w/ FS::part_event::Action::Mixin::credit_bill. +# this is a little different in that we calculate the credit on the whole +# invoice. + +sub calc_credit { + my $self = shift; + my $cust_bill = shift; + die "cust_bill record required" if !$cust_bill or !$cust_bill->custnum; + # count invoices before or including this one + my $cycle = FS::cust_bill->count('custnum = ? AND _date <= ?', + $cust_bill->custnum, + $cust_bill->_date + ); + my $rate = qsearchs('commission_rate', { + schedulenum => $self->schedulenum, + cycle => $cycle, + }); + # we might do something with a rate that applies "after the end of the + # schedule" (cycle = 0 or something) so that this can do commissions with + # no end date. add that here if there's a need. + return unless $rate; + + my $amount; + if ( $rate->percent ) { + my $what = $self->basis; + my $cost = ($what =~ /_cost/ ? 1 : 0); + my $margin = ($what =~ /_margin/ ? 1 : 0); + my %part_pkg_cache; + foreach my $cust_bill_pkg ( $cust_bill->cust_bill_pkg ) { + + my $charge = 0; + next if !$cust_bill_pkg->pkgnum; # exclude taxes and fees + + my $cust_pkg = $cust_bill_pkg->cust_pkg; + if ( $margin or $cost ) { + # look up package costs only if we need them + my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart; + my $part_pkg = $part_pkg_cache{$pkgpart} + ||= FS::part_pkg->by_key($pkgpart); + + if ( $cost ) { + $charge = $part_pkg->get($what); + } else { # $margin + $charge = $part_pkg->$what($cust_pkg); + } + + $charge = ($charge || 0) * ($cust_pkg->quantity || 1); + + } else { + + if ( $what eq 'setup' ) { + $charge = $cust_bill_pkg->get('setup'); + } elsif ( $what eq 'recur' ) { + $charge = $cust_bill_pkg->get('recur'); + } elsif ( $what eq 'setuprecur' ) { + $charge = $cust_bill_pkg->get('setup') + + $cust_bill_pkg->get('recur'); + } + } + + $amount += ($charge * $rate->percent / 100); + + } + } # if $rate->percent + + if ( $rate->amount ) { + $amount += $rate->amount; + } + + $amount = sprintf('%.2f', $amount + 0.005); + return $amount; +} + +=back + +=head1 SEE ALSO + +L, L, L + +=cut + +1; + diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm index 9a1f22a02..7c67c2df8 100644 --- a/FS/FS/cust_bill_pkg_tax_location.pm +++ b/FS/FS/cust_bill_pkg_tax_location.pm @@ -338,7 +338,7 @@ sub upgrade_taxable_billpkgnum { } #for $i } else { # the more complicated case - $log->warn("mismatched charges and tax links in pkg#$pkgnum", + $log->warning("mismatched charges and tax links in pkg#$pkgnum", object => $cust_bill); my $tax_amount = sum(map {$_->amount} @tax_links); # remove all tax link records and recreate them to be 1:1 with diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 85463724c..e4b1fc07d 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -315,6 +315,7 @@ sub check { || $self->ut_foreign_keyn('commission_agentnum', 'agent', 'agentnum') || $self->ut_foreign_keyn('commission_salesnum', 'sales', 'salesnum') || $self->ut_foreign_keyn('commission_pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_keyn('commission_invnum', 'cust_bill', 'invnum') ; return $error if $error; diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm index 90400984c..fdc2cf8da 100644 --- a/FS/FS/cust_location.pm +++ b/FS/FS/cust_location.pm @@ -2,7 +2,7 @@ package FS::cust_location; use base qw( FS::geocode_Mixin FS::Record ); use strict; -use vars qw( $import $DEBUG $conf $label_prefix ); +use vars qw( $import $DEBUG $conf $label_prefix $allow_location_edit ); use Data::Dumper; use Date::Format qw( time2str ); use FS::UID qw( dbh driver_name ); @@ -171,6 +171,10 @@ sub find_or_insert { delete $nonempty{'locationnum'}; my %hash = map { $_ => $self->get($_) } @essential; + foreach (values %hash) { + s/^\s+//; + s/\s+$//; + } my @matches = qsearch('cust_location', \%hash); # we no longer reject matches for having different values in nonessential @@ -292,7 +296,7 @@ sub replace { # it's a prospect location, then there are no active packages, no billing # history, no taxes, and in general no reason to keep the old location # around. - if ( $self->custnum ) { + if ( !$allow_location_edit and $self->custnum ) { foreach (qw(address1 address2 city state zip country)) { if ( $self->$_ ne $old->$_ ) { return "can't change cust_location field $_"; @@ -347,6 +351,10 @@ sub check { return '' if $self->disabled; # so that disabling locations never fails + # maybe should just do all fields in the table? + # or in every table? + $self->trim_whitespace(qw(district city county state country)); + my $error = $self->ut_numbern('locationnum') || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum') @@ -887,6 +895,35 @@ sub process_standardize { close $log; } +sub _upgrade_data { + my $class = shift; + + # are we going to need to update tax districts? + my $use_districts = $conf->config('tax_district_method') ? 1 : 0; + + # trim whitespace on records that need it + local $allow_location_edit = 1; + foreach my $field (qw(city county state country district)) { + foreach my $location (qsearch({ + table => 'cust_location', + extra_sql => " WHERE $field LIKE ' %' OR $field LIKE '% '" + })) { + my $error = $location->replace; + die "$error (fixing whitespace in $field, locationnum ".$location->locationnum.')' + if $error; + + if ( $use_districts ) { + my $queue = new FS::queue { + 'job' => 'FS::geocode_Mixin::process_district_update' + }; + $error = $queue->insert( 'FS::cust_location' => $location->locationnum ); + die $error if $error; + } + } # foreach $location + } # foreach $field + ''; +} + =head1 BUGS =head1 SEE ALSO diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 0fc2cb7e0..3e4a438d6 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -355,6 +355,35 @@ sub _bop_content { \%content; } +sub _tokenize_card { + my ($self,$transaction,$payinfo,$log) = @_; + + if ( $transaction->can('card_token') + and $transaction->card_token + and $payinfo !~ /^99\d{14}$/ #not already tokenized + ) { + + my @cust_payby = $self->cust_payby('CARD','DCRD'); + @cust_payby = grep { $payinfo == $_->payinfo } @cust_payby; + if (@cust_payby > 1) { + $log->error('Multiple matching card numbers for cust '.$self->custnum.', could not tokenize card'); + } elsif (@cust_payby) { + my $cust_payby = $cust_payby[0]; + $cust_payby->payinfo($transaction->card_token); + my $error = $cust_payby->replace; + if ( $error ) { + $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error); + } else { + $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum); + } + } else { + $log->debug('No matching card numbers for cust '.$self->custnum.', could not tokenize card'); + } + + } + +} + my %bop_method2payby = ( 'CC' => 'CARD', 'ECHECK' => 'CHEK', @@ -369,6 +398,8 @@ sub realtime_bop { unless $FS::UID::AutoCommit; local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; + + my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop'); my %options = (); if (ref($_[0]) eq 'HASH') { @@ -774,18 +805,7 @@ sub realtime_bop { # Tokenize ### - - if ( $transaction->can('card_token') && $transaction->card_token ) { - - if ( $options{'payinfo'} eq $self->payinfo ) { - $self->payinfo($transaction->card_token); - my $error = $self->replace; - if ( $error ) { - warn "WARNING: error storing token: $error, but proceeding anyway\n"; - } - } - - } + $self->_tokenize_card($transaction,$options{'payinfo'},$log); ### # result handling @@ -1950,6 +1970,7 @@ sub realtime_verify_bop { if ( $reverse->is_success ) { $cust_pay_pending->status('done'); + $cust_pay_pending->statustext('reversed'); my $cpp_authorized_err = $cust_pay_pending->replace; return $cpp_authorized_err if $cpp_authorized_err; @@ -2083,19 +2104,7 @@ sub realtime_verify_bop { # Tokenize ### - if ( $transaction->can('card_token') && $transaction->card_token ) { - - if ( $options{'payinfo'} eq $self->payinfo ) { - $self->payinfo($transaction->card_token); - my $error = $self->replace; - if ( $error ) { - my $warning = "WARNING: error storing token: $error, but proceeding anyway\n"; - $log->warning($warning); - warn $warning; - } - } - - } + $self->_tokenize_card($transaction,$options{'payinfo'},$log); ### # result handling diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 3c355e823..a1233d083 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -122,6 +122,9 @@ methods. sub check { my $self = shift; + $self->trim_whitespace(qw(district city county state country)); + $self->set('city', uc($self->get('city'))); # also county? + $self->exempt_amount(0) unless $self->exempt_amount; $self->ut_numbern('taxnum') @@ -701,6 +704,49 @@ sub _upgrade_data { } FS::upgrade_journal->set_done($journal); } + # trim whitespace and convert to uppercase in the 'city' field. + foreach my $record (qsearch({ + table => 'cust_main_county', + extra_sql => " WHERE city LIKE ' %' OR city LIKE '% ' OR city != UPPER(city)", + })) { + # any with-trailing-space records probably duplicate other records + # from the same city, and if we just fix the record in place, we'll + # create an exact duplicate. + # so find the record this one would duplicate, and merge them. + $record->check; # trims whitespace + my %match = map { $_ => $record->get($_) } + qw(city county state country district taxname taxclass); + my $other = qsearchs('cust_main_county', \%match); + if ($other) { + my $new_taxnum = $other->taxnum; + my $old_taxnum = $record->taxnum; + if ($other->tax != $record->tax or + $other->exempt_amount != $record->exempt_amount) { + # don't assume these are the same. + warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n"; + } else { + warn "Merging tax #$old_taxnum into #$new_taxnum\n"; + foreach my $table (qw( + cust_bill_pkg_tax_location + cust_bill_pkg_tax_location_void + cust_tax_exempt_pkg + cust_tax_exempt_pkg_void + )) { + foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) { + $row->set('taxnum' => $new_taxnum); + my $error = $row->replace; + die $error if $error; + } + } + my $error = $record->delete; + die $error if $error; + } + } else { + # else there is no record this one duplicates, so just fix it + my $error = $record->replace; + die $error if $error; + } + } # foreach $record ''; } diff --git a/FS/FS/cust_pay_pending.pm b/FS/FS/cust_pay_pending.pm index dfb07b84d..3a8322e06 100644 --- a/FS/FS/cust_pay_pending.pm +++ b/FS/FS/cust_pay_pending.pm @@ -455,6 +455,26 @@ sub decline { $self->replace; } +=item reverse [ STATUSTEXT ] + +Sets the status of this pending payment to "done" (with statustext +"reversed (manual)" unless otherwise specified). + +Currently only used when resolving pending payments manually. + +=cut + +# almost complete false laziness with decline, +# but want to avoid confusion, in case any additional steps/defaults are ever added to either +sub reverse { + my $self = shift; + my $statustext = shift || "reversed (manual)"; + + $self->status('done'); + $self->statustext($statustext); + $self->replace; +} + # _upgrade_data # # Used by FS::Upgrade to migrate to a new database. diff --git a/FS/FS/cust_pkg_reason.pm b/FS/FS/cust_pkg_reason.pm index 29b4b0a91..a632ab415 100644 --- a/FS/FS/cust_pkg_reason.pm +++ b/FS/FS/cust_pkg_reason.pm @@ -3,7 +3,7 @@ use base qw( FS::otaker_Mixin FS::Record ); use strict; use vars qw( $ignore_empty_action ); -use FS::Record qw( qsearch ); #qsearchs ); +use FS::Record qw( qsearch qsearchs ); use FS::upgrade_journal; $ignore_empty_action = 0; diff --git a/FS/FS/log.pm b/FS/FS/log.pm index 1d4df730a..d432ee3c6 100644 --- a/FS/FS/log.pm +++ b/FS/FS/log.pm @@ -6,6 +6,8 @@ use FS::Record qw( qsearch qsearchs dbdef ); use FS::UID qw( dbh driver_name ); use FS::log_context; use FS::log_email; +use FS::upgrade_journal; +use Tie::IxHash; =head1 NAME @@ -115,7 +117,7 @@ sub insert { 'msgtype' => 'admin', 'to' => $log_email->to_addr, 'substitutions' => { - 'loglevel' => $FS::Log::LEVELS[$self->level], # which has hopefully been loaded... + 'loglevel' => $FS::Log::LEVELS{$self->level}, # which has hopefully been loaded... 'logcontext' => $log_email->context, # use the one that triggered the email 'logmessage' => $self->message, }, @@ -383,6 +385,49 @@ sub search { }; } +sub _upgrade_data { + my ($class, %opts) = @_; + + return if FS::upgrade_journal->is_done('log__remap_levels'); + + tie my %levelmap, 'Tie::IxHash', + 2 => 1, #notice -> info + 6 => 5, #alert -> critical + 7 => 5, #emergency -> critical + ; + + # this method should never autocommit + # should have been set in upgrade, but just in case... + local $FS::UID::AutoCommit = 0; + + # in practice, only debug/info/warning/error appear to have been used, + # so this probably won't do anything, but just in case + foreach my $old (keys %levelmap) { + # FS::log has no replace method + my $sql = 'UPDATE log SET level=' . dbh->quote($levelmap{$old}) . ' WHERE level=' . dbh->quote($old); + warn $sql unless $opts{'quiet'}; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute() or die $sth->errstr; + $sth->finish(); + } + + foreach my $log_email ( + qsearch('log_email',{ 'min_level' => 2 }), + qsearch('log_email',{ 'min_level' => 6 }), + qsearch('log_email',{ 'min_level' => 7 }), + ) { + $log_email->min_level($levelmap{$log_email->min_level}); + my $error = $log_email->replace; + if ($error) { + dbh->rollback; + die $error; + } + } + + FS::upgrade_journal->set_done('log__remap_levels'); + +} + =back =head1 BUGS diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm index 83414a680..37befb515 100644 --- a/FS/FS/log_context.pm +++ b/FS/FS/log_context.pm @@ -5,10 +5,10 @@ use base qw( FS::Record ); use FS::Record qw( qsearch qsearchs ); my @contexts = ( qw( - test bill_and_collect FS::cust_main::Billing::bill_and_collect FS::cust_main::Billing::bill + FS::cust_main::Billing_Realtime::realtime_bop FS::cust_main::Billing_Realtime::realtime_verify_bop FS::pay_batch::import_from_gateway FS::part_pkg @@ -23,6 +23,7 @@ my @contexts = ( qw( upgrade_taxable_billpkgnum freeside-paymentech-upload freeside-paymentech-download + test ) ); =head1 NAME diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index b89071710..0a16724a8 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -804,6 +804,59 @@ sub _upgrade_data { ### $self->_populate_initial_data; + ### + # Move welcome_msgnum to an export + ### + + #upgrade_journal loaded by _populate_initial_data + unless (FS::upgrade_journal->is_done('msg_template__welcome_export')) { + if (my $msgnum = $conf->config('welcome_msgnum')) { + eval "use FS::part_export;"; + die $@ if $@; + eval "use FS::part_svc;"; + die $@ if $@; + eval "use FS::export_svc;"; + die $@ if $@; + #create the export + my $part_export = new FS::part_export { + 'exportname' => 'Welcome Email', + 'exporttype' => 'send_email' + }; + my $error = $part_export->insert({ + 'to_customer' => 1, + 'insert_template' => $msgnum, + # replicate blank options that would be generated by UI, + # to avoid unexpected results from not having them exist + 'to_address' => '', + 'replace_template' => 0, + 'suspend_template' => 0, + 'unsuspend_template' => 0, + 'delete_template' => 0, + }); + die $error if $error; + #attach it to part_svcs + my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude'); + foreach my $part_svc ( + qsearch('part_svc',{ 'svcdb' => 'svc_acct', 'disabled' => '' }) + ) { + next if grep { $_ eq $part_svc->svcpart } @welcome_exclude_svcparts; + my $export_svc = new FS::export_svc { + 'exportnum' => $part_export->exportnum, + 'svcpart' => $part_svc->svcpart, + }; + $error = $export_svc->insert; + die $error if $error; + } + #remove the old confs + $error = $conf->delete('welcome_msgnum'); + die $error if $error; + $error = $conf->delete('svc_acct_welcome_exclude'); + die $error if $error; + } + FS::upgrade_journal->set_done('msg_template__welcome_export'); + } + + ### Fix dump-email_to (needs to happen after _populate_initial_data) if ($conf->config('dump-email_to')) { # anyone who still uses dump-email_to should have just had this created diff --git a/FS/FS/part_event/Action/bill_agent_credit_schedule.pm b/FS/FS/part_event/Action/bill_agent_credit_schedule.pm new file mode 100644 index 000000000..31189a237 --- /dev/null +++ b/FS/FS/part_event/Action/bill_agent_credit_schedule.pm @@ -0,0 +1,76 @@ +package FS::part_event::Action::bill_agent_credit_schedule; + +use base qw( FS::part_event::Action ); +use FS::Conf; +use FS::cust_credit; +use FS::commission_schedule; +use Date::Format qw(time2str); + +use strict; + +sub description { 'Credit the agent based on a commission schedule' } + +sub option_fields { + 'schedulenum' => { 'label' => 'Schedule', + 'type' => 'select-table', + 'table' => 'commission_schedule', + 'name_col' => 'schedulename', + 'disable_empty'=> 1, + }, +} + +sub eventtable_hashref { + { 'cust_bill' => 1 }; +} + +our $date_format; + +sub do_action { + my( $self, $cust_bill, $cust_event ) = @_; + + $date_format ||= FS::Conf->new->config('date_format') || '%x'; + + my $cust_main = $self->cust_main($cust_bill); + my $agent = $cust_main->agent; + return "No customer record for agent ". $agent->agent + unless $agent->agent_custnum; + + my $agent_cust_main = $agent->agent_cust_main; + + my $schedulenum = $self->option('schedulenum') + or return "no commission schedule selected"; + my $schedule = FS::commission_schedule->by_key($schedulenum) + or return "commission schedule #$schedulenum not found"; + # commission_schedule::delete tries to prevent this, but just in case + + my $amount = $schedule->calc_credit($cust_bill) + or return; + + my $reasonnum = $schedule->reasonnum; + + #XXX shouldn't do this here, it's a localization problem. + # credits with commission_invnum should know how to display it as part + # of invoice rendering. + my $desc = 'from invoice #'. $cust_bill->display_invnum . + ' ('. time2str($date_format, $cust_bill->_date) . ')'; + # could also show custnum and pkgnums here? + my $cust_credit = FS::cust_credit->new({ + 'custnum' => $agent_cust_main->custnum, + 'reasonnum' => $reasonnum, + 'amount' => $amount, + 'eventnum' => $cust_event->eventnum, + 'addlinfo' => $desc, + 'commission_agentnum' => $cust_main->agentnum, + 'commission_invnum' => $cust_bill->invnum, + }); + my $error = $cust_credit->insert; + die "Error crediting customer ". $agent_cust_main->custnum. + " for agent commission: $error" + if $error; + + #return $warning; # currently don't get warnings here + return; + +} + +1; diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 4f26e8c6f..5f7ce3550 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -420,15 +420,30 @@ sub paydate_epoch_sql { Find all records with a credit card payment type and no paycardtype, and replace them in order to set their paycardtype. +This method actually just starts a queue job. + =cut sub upgrade_set_cardtype { my $class = shift; + my $table = $class->table or die "upgrade_set_cardtype needs a table"; + + if ( ! FS::upgrade_journal->is_done("${table}__set_cardtype") ) { + my $job = FS::queue->new({ job => 'FS::payinfo_Mixin::process_set_cardtype' }); + my $error = $job->insert($table); + die $error if $error; + FS::upgrade_journal->set_done("${table}__set_cardtype"); + } +} + +sub process_set_cardtype { + my $table = shift; + # assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype # will do this. ignore any problems with the cards. local $ignore_masked_payinfo = 1; my $search = FS::Cursor->new({ - table => $class->table, + table => $table, extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ], }); while (my $record = $search->fetch) { diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm index 1dd9ffb63..f2456a56f 100644 --- a/FS/FS/svc_Common.pm +++ b/FS/FS/svc_Common.pm @@ -1481,8 +1481,12 @@ sub search { } #svcnum - if ( $params->{'svcnum'} =~ /^(\d+)$/ ) { - push @where, "svcnum = $1"; + if ( $params->{'svcnum'} ) { + my @svcnum = ref( $params->{'svcnum'} ) + ? @{ $params->{'svcnum'} } + : $params->{'svcnum'}; + @svcnum = grep /^\d+$/, @svcnum; + push @where, 'svcnum IN ('. join(',', @svcnum) . ')' if @svcnum; } # svcpart diff --git a/FS/MANIFEST b/FS/MANIFEST index 83359f118..4184b9ce6 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -870,3 +870,7 @@ FS/webservice_log.pm t/webservice_log.t FS/access_user_page_pref.pm t/access_user_page_pref.t +FS/commission_schedule.pm +t/commission_schedule.t +FS/commission_rate.pm +t/commission_rate.t diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited index 008759008..1745d67a0 100644 --- a/FS/bin/freeside-cdrrewrited +++ b/FS/bin/freeside-cdrrewrited @@ -4,7 +4,7 @@ use strict; use vars qw( $conf ); use FS::Daemon ':all'; #daemonize1 drop_root daemonize2 myexit logfile sig* use FS::UID qw( adminsuidsetup ); -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs dbh ); #use FS::cdr; #use FS::cust_pkg; #use FS::queue; @@ -24,12 +24,12 @@ daemonize2(); $conf = new FS::Conf; -die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ". - " and cdr-taqua-accountcode_rewrite conf options are all off\n" +die "not running; relevant conf options are all off\n" unless _shouldrun(); #-- +#used for taqua my %sessionnum_unmatch = (); my $sessionnum_retry = 4 * 60 * 60; # 4 hours my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days @@ -45,20 +45,25 @@ while (1) { # instead of just doing this search like normal CDRs #hmm :/ + #used only by taqua, should have no effect otherwise my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time } keys %sessionnum_unmatch; my $extra_sql = scalar(@recent) ? ' AND acctid NOT IN ('. join(',', @recent). ') ' : ''; + #order matters for removing dupes--only the first is preserved + $extra_sql .= ' ORDER BY acctid ' + if $conf->exists('cdr-skip_duplicate_rewrite'); + my $found = 0; - my %skip = (); + my %skip = (); #used only by taqua my %warning = (); foreach my $cdr ( qsearch( { 'table' => 'cdr', - 'extra_sql' => 'FOR UPDATE', + 'extra_sql' => 'FOR UPDATE', #XXX overwritten by opt below...would fixing this break anything? 'hashref' => {}, 'extra_sql' => 'WHERE freesidestatus IS NULL '. ' AND freesiderewritestatus IS NULL '. @@ -67,11 +72,27 @@ while (1) { } ) ) { - next if $skip{$cdr->acctid}; + next if $skip{$cdr->acctid}; #used only by taqua $found = 1; my @status = (); + if ($conf->exists('cdr-skip_duplicate_rewrite')) { + #qsearch can't handle timestamp type of calldate + my $sth = dbh->prepare( + 'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1' + ) or die dbh->errstr; + $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr; + my $isdup = $sth->fetchrow_hashref; + $sth->finish; + if ($isdup) { + #we only act on this cdr, not touching previous dupes + #if a dupe somehow creeped in previously, too late to fix it + $cdr->freesidestatus('done'); #prevent it from being billed + push(@status,'duplicate'); + } + } + if ( $conf->exists('cdr-asterisk_forward_rewrite') && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst ) @@ -240,6 +261,7 @@ sub _shouldrun { || $conf->exists('cdr-taqua-accountcode_rewrite') || $conf->exists('cdr-taqua-callerid_rewrite') || $conf->exists('cdr-intl_to_domestic_rewrite') + || $conf->exists('cdr-skip_duplicate_rewrite') || 0 ; } @@ -263,6 +285,11 @@ of the following config options are enabled: =over 4 +=item cdr-skip_duplicate_rewrite + +Marks as 'done' (prevents billing for) any CDRs with +a src, dst and calldate identical to an existing CDR + =item cdr-asterisk_australia_rewrite Classifies Australian numbers as domestic, mobile, tollfree, international, or diff --git a/FS/t/commission_rate.t b/FS/t/commission_rate.t new file mode 100644 index 000000000..fb5f43cc5 --- /dev/null +++ b/FS/t/commission_rate.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::commission_rate; +$loaded=1; +print "ok 1\n"; diff --git a/FS/t/commission_schedule.t b/FS/t/commission_schedule.t new file mode 100644 index 000000000..bbe6b42dc --- /dev/null +++ b/FS/t/commission_schedule.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::commission_schedule; +$loaded=1; +print "ok 1\n"; diff --git a/bin/xmlrpc-insert_payby b/bin/xmlrpc-insert_payby new file mode 100755 index 000000000..9815d0551 --- /dev/null +++ b/bin/xmlrpc-insert_payby @@ -0,0 +1,52 @@ +#!/usr/bin/perl + +use strict; +use Frontier::Client; +use Data::Dumper; + +use Getopt::Long; + +my( $email, $password ) = @ARGV; +die "Usage: xmlrpc-insert_payby email password + [-w weight -b payby -i payinfo -c paycvv -d paydate -n payname -s paystate -t paytype -p payip]\n" + unless $email && length($password); + +my %opts; +GetOptions( + "by=s" => \$opts{'payby'}, + "cvv=s" => \$opts{'paycvv'}, + "date=s" => \$opts{'paydate'}, + "info=s" => \$opts{'payinfo'}, + "name=s" => \$opts{'payname'}, + "payip=s" => \$opts{'payip'}, + "state=s" => \$opts{'paystate'}, + "type=s" => \$opts{'paytype'}, + "weight=i" => \$opts{'weight'}, +); + +foreach my $key (keys %opts) { + delete($opts{$key}) unless defined($opts{$key}); +} + +my $uri = new URI 'http://localhost:8080/'; + +my $server = new Frontier::Client ( 'url' => $uri ); + +my $login_result = $server->call( + 'FS.ClientAPI_XMLRPC.login', + 'email' => $email, + 'password' => $password, +); +die $login_result->{'error'}."\n" if $login_result->{'error'}; + +my $call_result = $server->call( + 'FS.ClientAPI_XMLRPC.insert_payby', + 'session_id' => $login_result->{'session_id'}, + %opts, +); +die $call_result->{'error'}."\n" if $call_result->{'error'}; + +print Dumper($call_result); +print "Successfully inserted\n"; + +1; diff --git a/bin/xmlrpc-update_payby b/bin/xmlrpc-update_payby new file mode 100755 index 000000000..75a1a8da9 --- /dev/null +++ b/bin/xmlrpc-update_payby @@ -0,0 +1,53 @@ +#!/usr/bin/perl + +use strict; +use Frontier::Client; +use Data::Dumper; + +use Getopt::Long; + +my( $email, $password, $custpaybynum ) = @ARGV; +die "Usage: xmlrpc-update_payby email password custpaybynum + [-w weight -b payby -i payinfo -c paycvv -d paydate -n payname -s paystate -t paytype -p payip]\n" + unless $email && length($password) && $custpaybynum; + +my %opts; +GetOptions( + "by=s" => \$opts{'payby'}, + "cvv=s" => \$opts{'paycvv'}, + "date=s" => \$opts{'paydate'}, + "info=s" => \$opts{'payinfo'}, + "name=s" => \$opts{'payname'}, + "payip=s" => \$opts{'payip'}, + "state=s" => \$opts{'paystate'}, + "type=s" => \$opts{'paytype'}, + "weight=i" => \$opts{'weight'}, +); + +foreach my $key (keys %opts) { + delete($opts{$key}) unless defined($opts{$key}); +} + +my $uri = new URI 'http://localhost:8080/'; + +my $server = new Frontier::Client ( 'url' => $uri ); + +my $login_result = $server->call( + 'FS.ClientAPI_XMLRPC.login', + 'email' => $email, + 'password' => $password, +); +die $login_result->{'error'}."\n" if $login_result->{'error'}; + +my $call_result = $server->call( + 'FS.ClientAPI_XMLRPC.update_payby', + 'session_id' => $login_result->{'session_id'}, + 'custpaybynum' => $custpaybynum, + %opts, +); +die $call_result->{'error'}."\n" if $call_result->{'error'}; + +print Dumper($call_result); +print "Successfully updated\n"; + +1; diff --git a/debian/control b/debian/control index 24ddea319..4de8fee57 100644 --- a/debian/control +++ b/debian/control @@ -28,7 +28,7 @@ Description: Billing and trouble ticketing for service providers Package: freeside-lib Architecture: all Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,latex-xcolor, - libbusiness-creditcard-perl,libcache-cache-perl, + libbusiness-creditcard-perl (>= 0.36),libcache-cache-perl, libcache-simple-timedexpiry-perl,libchart-perl,libclass-container-perl, libclass-data-inheritable-perl,libclass-returnvalue-perl,libcolor-scheme-perl, libcompress-zlib-perl,libconvert-binhex-perl,libcrypt-passwdmd5-perl, diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index bc54b1ef3..3be4ebd8b 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -50,6 +50,7 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'list_invoices' => 'MyAccount/list_invoices', #? 'list_payby' => 'MyAccount/list_payby', 'insert_payby' => 'MyAccount/insert_payby', + 'update_payby' => 'MyAccount/update_payby', 'delete_payby' => 'MyAccount/delete_payby', 'cancel' => 'MyAccount/cancel', #add to ss cgi! 'payment_info' => 'MyAccount/payment_info', @@ -682,6 +683,16 @@ Optional IP address from which payment was submitted If there is an error, returns a hash reference with a single key, B, otherwise returns a hash reference with a single key, B. +=item update_payby HASHREF + +Updates stored payment information. Takes a hash reference with the same +keys as insert_payby, as well as B to specify which record +to update. All keys except B and B are optional; +if omitted, the previous values in the record will be preserved. + +If there is an error, returns a hash reference with a single key, B, +otherwise returns a hash reference with a single key, B. + =item delete_payby HASHREF Removes stored payment information. Takes a hash reference with two keys, diff --git a/httemplate/browse/commission_schedule.html b/httemplate/browse/commission_schedule.html new file mode 100644 index 000000000..5a4f9840e --- /dev/null +++ b/httemplate/browse/commission_schedule.html @@ -0,0 +1,70 @@ +<& elements/browse.html, + 'title' => "Commission schedules", + 'name' => "commission schedules", + 'menubar' => [ 'Add a new schedule' => + $p.'edit/commission_schedule.html' + ], + 'query' => { 'table' => 'commission_schedule', }, + 'count_query' => 'SELECT COUNT(*) FROM commission_schedule', + 'header' => [ '#', + 'Name', + 'Rates', + ], + 'fields' => [ 'schedulenum', + 'schedulename', + $rates_sub, + ], + 'links' => [ $link, + $link, + '', + ], + 'disable_total' => 1, +&> +<%init> + +my $money_char = FS::Conf->new->config('money_char') || '$'; + +my $ordinal_sub = sub { + # correct from 1 to 12... + my $num = shift; + $num == 1 ? '1st' : + $num == 2 ? '2nd' : + $num == 3 ? '3rd' : + $num . 'th' +}; + +my $rates_sub = sub { + my $schedule = shift; + my @rates = sort { $a->cycle <=> $b->cycle } $schedule->commission_rate; + my @data; + my $basis = emt(lc( $FS::commission_schedule::basis_options{$schedule->basis} )); + foreach my $rate (@rates) { + my $desc = ''; + if ( $rate->amount > 0 ) { + $desc = $money_char . sprintf('%.2f', $rate->amount); + } + if ( $rate->percent > 0 ) { + $desc .= ' + ' if $desc; + $desc .= $rate->percent . '% ' . emt('of') . ' ' . $basis; + } + next if !$desc; + $desc = &$ordinal_sub($rate->cycle) . ' ' . emt('invoice') . + ': ' . $desc; + + push @data, + [ + { + 'data' => $desc, + 'align' => 'right', + } + ]; + } + \@data; +}; + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $link = [ $p.'edit/commission_schedule.html?', 'schedulenum' ]; + + diff --git a/httemplate/browse/log_email.html b/httemplate/browse/log_email.html index 0f64dd454..007ea6f74 100644 --- a/httemplate/browse/log_email.html +++ b/httemplate/browse/log_email.html @@ -21,7 +21,7 @@ ], 'fields' => [ 'logemailnum', sub { $_[0]->context || '(all)' }, - sub { $FS::Log::LEVELS[$_[0]->min_level] }, + sub { $FS::Log::LEVELS{$_[0]->min_level} }, 'msgname', 'to_addr', $actions, diff --git a/httemplate/edit/commission_schedule.html b/httemplate/edit/commission_schedule.html new file mode 100644 index 000000000..c76a3618e --- /dev/null +++ b/httemplate/edit/commission_schedule.html @@ -0,0 +1,53 @@ +<& elements/edit.html, + name_singular => 'schedule', + table => 'commission_schedule', + viewall_dir => 'browse', + fields => [ 'schedulename', + { field => 'reasonnum', + type => 'select-reason', + reason_class => 'R', + }, + { field => 'basis', + type => 'select', + options => [ keys %FS::commission_schedule::basis_options ], + labels => { %FS::commission_schedule::basis_options }, + }, + { type => 'tablebreak-tr-title', value => 'Billing cycles' }, + { field => 'commissionratenum', + type => 'commission_rate', + o2m_table => 'commission_rate', + m2_label => ' ', + m2_error_callback => $m2_error_callback, + colspan => 2, + }, + ], + labels => { 'schedulenum' => '', + 'schedulename' => 'Name', + 'basis' => 'Based on', + 'commissionratenum' => '', + }, +&> +<%init> + +my $m2_error_callback = sub { + my ($cgi, $object) = @_; + + my @rates; + foreach my $k ( grep /^commissionratenum\d+/, $cgi->param ) { + my $num = $cgi->param($k); + my $cycle = $cgi->param($k.'_cycle'); + my $amount = $cgi->param($k.'_amount'); + my $percent = $cgi->param($k.'_percent'); + if ($cycle > 0) { + push @rates, FS::commission_rate->new({ + 'commissionratenum' => $num, + 'cycle' => $cycle, + 'amount' => $amount, + 'percent' => $percent, + }); + } + } + @rates; +}; + + diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi index 39cddc021..b314d2d6e 100755 --- a/httemplate/edit/cust_main.cgi +++ b/httemplate/edit/cust_main.cgi @@ -203,12 +203,19 @@ if ( $cgi->param('error') ) { my %locations; for my $pre (qw(bill ship)) { my %hash; - foreach ( FS::cust_main->location_fields ) { - $hash{$_} = scalar($cgi->param($pre.'_'.$_)); + foreach my $locfield ( FS::cust_main->location_fields ) { + # don't search on lat/long, string values can cause qsearchs to die + next if grep {$_ eq $locfield} qw(latitude longitude); + $hash{$locfield} = scalar($cgi->param($pre.'_'.$locfield)); } $hash{'custnum'} = $cgi->param('custnum'); $locations{$pre} = qsearchs('cust_location', \%hash) || FS::cust_location->new( \%hash ); + # now set lat/long, for redisplay of entered values + foreach my $locfield ( qw(latitude longitude) ) { + my $locvalue = scalar($cgi->param($pre.'_'.$locfield)); + $locations{$pre}->set($locfield,$locvalue); + } } if ( $same ) { $locations{ship} = $locations{bill}; diff --git a/httemplate/edit/cust_pay_pending.html b/httemplate/edit/cust_pay_pending.html index 0056bb925..7d480f319 100644 --- a/httemplate/edit/cust_pay_pending.html +++ b/httemplate/edit/cust_pay_pending.html @@ -4,6 +4,10 @@
Are you sure you want to delete this pending payment?
+% } elsif (( $action eq 'complete' ) and $authorized) { + +
Payment was authorized but not captured. Contact <% $cust_pay_pending->processor || 'the payment gateway' %> to establish the final disposition of this transaction.
+ % } elsif ( $action eq 'complete' ) {
No response was received from <% $cust_pay_pending->processor || 'the payment gateway' %> for this transaction. Check <% $cust_pay_pending->processor || 'the payment gateway' %>'s reporting and determine if this transaction completed successfully.
@@ -97,8 +101,6 @@ % } else { -%# if ( $action eq 'complete' ) { - @@ -106,18 +108,25 @@ -% if ( $action eq 'complete' ) { +% if ( $action eq 'complete' ) {     +% if ($authorized) { + + + +% } else { +% }     - % } + + @@ -156,6 +165,8 @@ my $cust_pay_pending = }) or die 'unknown paypendingnum'; +my $authorized = ($cust_pay_pending->status eq 'authorized') ? 1 : 0; + my $conf = new FS::Conf; my $money_char = $conf->config('money_char') || '$'; diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html index bbc9797dc..8dd15dcfb 100644 --- a/httemplate/edit/elements/edit.html +++ b/httemplate/edit/elements/edit.html @@ -650,7 +650,7 @@ Example: var newrow = <% include(@layer_opt, html_only=>1) |js_string %>; % #until the rest have html/js_only -% if ( $type eq 'selectlayers' || $type =~ /^select-cgp_rule_/ ) { +% if ( ($type eq 'selectlayers') || ($type eq 'selectlayersx') || ($type =~ /^select-cgp_rule_/) ) { var newfunc = <% include(@layer_opt, js_only=>1) |js_string %>; % } else { var newfunc = ''; diff --git a/httemplate/edit/log_email.html b/httemplate/edit/log_email.html index 0c98046d3..b79aba986 100644 --- a/httemplate/edit/log_email.html +++ b/httemplate/edit/log_email.html @@ -16,8 +16,8 @@ }, { 'field' => 'min_level', 'type' => 'select', - 'options' => [ 0..7 ], - 'labels' => { map {$_ => $FS::Log::LEVELS[$_]} 0..7 }, + 'options' => [ &FS::Log::levelnums ], + 'labels' => { &FS::Log::levelmap }, 'curr_value' => scalar($cgi->param('min_level')), }, 'to_addr', diff --git a/httemplate/edit/part_event.html b/httemplate/edit/part_event.html index 47b8c1ac8..c8072e9f9 100644 --- a/httemplate/edit/part_event.html +++ b/httemplate/edit/part_event.html @@ -31,7 +31,7 @@ value => 'Event Conditions', }, { field => 'conditionname', - type => 'selectlayers', + type => 'selectlayersx', options => [ keys %all_conditions ], labels => \%condition_labels, onchange => 'condition_changed(what);', @@ -51,7 +51,7 @@ value => 'Event Action', }, { field => 'action', - type => 'selectlayers', + type => 'selectlayersx', options => [ keys %all_actions ], labels => \%action_labels, onchange => 'action_changed(what);', diff --git a/httemplate/edit/process/bulk-cust_main_county.html b/httemplate/edit/process/bulk-cust_main_county.html index b7ff40fa7..b5a0258b1 100644 --- a/httemplate/edit/process/bulk-cust_main_county.html +++ b/httemplate/edit/process/bulk-cust_main_county.html @@ -12,7 +12,7 @@ <% include('/elements/header-popup.html', "Taxes ${action}ed") %> diff --git a/httemplate/edit/process/bulk-cust_svc-pkgnum.html b/httemplate/edit/process/bulk-cust_svc-pkgnum.html index f5cf7dd07..3c273069a 100644 --- a/httemplate/edit/process/bulk-cust_svc-pkgnum.html +++ b/httemplate/edit/process/bulk-cust_svc-pkgnum.html @@ -7,7 +7,7 @@ <% header(emt("Services moved")) %> diff --git a/httemplate/edit/process/cgp_rule-simplified.html b/httemplate/edit/process/cgp_rule-simplified.html index 60769d4e6..24515d551 100644 --- a/httemplate/edit/process/cgp_rule-simplified.html +++ b/httemplate/edit/process/cgp_rule-simplified.html @@ -4,7 +4,7 @@ % } else { #success XXX better msg talking about vacation vs. redirect all <% include('/elements/header-popup.html', 'Rule updated') %> diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html index 308ea8ffd..54cafbf18 100644 --- a/httemplate/edit/process/change-cust_pkg.html +++ b/httemplate/edit/process/change-cust_pkg.html @@ -5,7 +5,7 @@ <% header(emt("Package changed")) %> diff --git a/httemplate/edit/process/commission_schedule.html b/httemplate/edit/process/commission_schedule.html new file mode 100644 index 000000000..50e0371da --- /dev/null +++ b/httemplate/edit/process/commission_schedule.html @@ -0,0 +1,36 @@ +<& elements/process.html, + 'table' => 'commission_schedule', + 'viewall_dir' => 'browse', + 'process_o2m' => { + 'table' => 'commission_rate', + 'fields' => [qw( cycle amount percent )], + }, + 'precheck_callback' => $precheck, + 'debug' => 1, +&> +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $precheck = sub { + my $cgi = shift; + $cgi->param('reasonnum') =~ /^(-?\d+)$/ or die "Illegal reasonnum"; + + my ($reasonnum, $error) = $m->comp('/misc/process/elements/reason'); + if (!$reasonnum) { + $error ||= 'Reason required' + } + $cgi->param('reasonnum', $reasonnum) unless $error; + + # remove rate entries with no cycle selected + foreach my $k (grep /^commissionratenum\d+$/, $cgi->param) { + if (! $cgi->param($k.'_cycle') ) { + $cgi->delete($k); + } + } + + $error; +}; + + diff --git a/httemplate/edit/process/credit-cust_bill_pkg.html b/httemplate/edit/process/credit-cust_bill_pkg.html index 75900bde5..12b68c0f5 100644 --- a/httemplate/edit/process/credit-cust_bill_pkg.html +++ b/httemplate/edit/process/credit-cust_bill_pkg.html @@ -3,7 +3,7 @@ %} else { <& /elements/header-popup.html, 'Credit successful' &> % } diff --git a/httemplate/edit/process/cust_credit-pkgnum.html b/httemplate/edit/process/cust_credit-pkgnum.html index 8941cbc73..56f7989a4 100755 --- a/httemplate/edit/process/cust_credit-pkgnum.html +++ b/httemplate/edit/process/cust_credit-pkgnum.html @@ -4,7 +4,7 @@ %} else { <% header(emt('Credit package changed')) %> diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi index 39c6f1997..5d3028777 100755 --- a/httemplate/edit/process/cust_credit.cgi +++ b/httemplate/edit/process/cust_credit.cgi @@ -16,7 +16,7 @@ % <% header(emt('Credit successful')) %> diff --git a/httemplate/edit/process/cust_location-censustract.html b/httemplate/edit/process/cust_location-censustract.html index bc9cd4f31..6edaca3fd 100644 --- a/httemplate/edit/process/cust_location-censustract.html +++ b/httemplate/edit/process/cust_location-censustract.html @@ -5,7 +5,7 @@ <% header("Census tract changed") %> diff --git a/httemplate/edit/process/cust_location.cgi b/httemplate/edit/process/cust_location.cgi index fd1b8740e..3a2388111 100644 --- a/httemplate/edit/process/cust_location.cgi +++ b/httemplate/edit/process/cust_location.cgi @@ -5,7 +5,7 @@ <% header("Location changed") %> diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi index 04516e984..74f8f2382 100755 --- a/httemplate/edit/process/cust_main.cgi +++ b/httemplate/edit/process/cust_main.cgi @@ -1,5 +1,15 @@ % if ( $error ) { % $cgi->param('error', $error); +% # workaround for create_uri_query's mangling of unicode characters, +% # false laziness with FS::Record::ut_coord +% use charnames ':full'; +% for my $pre (qw(bill ship)) { +% foreach (qw( latitude longitude)) { +% my $coord = $cgi->param($pre.'_'.$_); +% $coord =~ s/\N{DEGREE SIGN}\s*$//; +% $cgi->param($pre.'_'.$_, $coord); +% } +% } % my $query = $m->scomp('/elements/create_uri_query', 'secure'=>1); <% $cgi->redirect(popurl(2). "cust_main.cgi?$query" ) %> % diff --git a/httemplate/edit/process/cust_main_attach.cgi b/httemplate/edit/process/cust_main_attach.cgi index 09c18adcb..569500246 100644 --- a/httemplate/edit/process/cust_main_attach.cgi +++ b/httemplate/edit/process/cust_main_attach.cgi @@ -9,7 +9,7 @@ % $act = 'deleted' if($attachnum and $delete); <% header('Attachment ' . $act ) %> % } diff --git a/httemplate/edit/process/cust_main_county-add.cgi b/httemplate/edit/process/cust_main_county-add.cgi index fc8956b0c..fcc138f49 100755 --- a/httemplate/edit/process/cust_main_county-add.cgi +++ b/httemplate/edit/process/cust_main_county-add.cgi @@ -1,7 +1,7 @@ <% include('/elements/header-popup.html', 'Addition successful' ) %> diff --git a/httemplate/edit/process/cust_main_county-expand.cgi b/httemplate/edit/process/cust_main_county-expand.cgi index a10827621..42e46734a 100755 --- a/httemplate/edit/process/cust_main_county-expand.cgi +++ b/httemplate/edit/process/cust_main_county-expand.cgi @@ -1,7 +1,7 @@ <% include('/elements/header-popup.html', 'Addition successful' ) %> diff --git a/httemplate/edit/process/cust_main_note.cgi b/httemplate/edit/process/cust_main_note.cgi index 53e616a43..bb52db8f3 100755 --- a/httemplate/edit/process/cust_main_note.cgi +++ b/httemplate/edit/process/cust_main_note.cgi @@ -4,7 +4,7 @@ %} else { <% header('Note ' . ($notenum ? 'updated' : 'added') ) %> % } diff --git a/httemplate/edit/process/cust_pay-no_auto_apply.cgi b/httemplate/edit/process/cust_pay-no_auto_apply.cgi index ccbd2d7b5..4a5ee841a 100644 --- a/httemplate/edit/process/cust_pay-no_auto_apply.cgi +++ b/httemplate/edit/process/cust_pay-no_auto_apply.cgi @@ -15,7 +15,7 @@ Requires 'Apply payment' acl.

<% emt($message) %>

<% emt('Please wait while the page reloads.') %>

% } diff --git a/httemplate/edit/process/cust_pay-pkgnum.html b/httemplate/edit/process/cust_pay-pkgnum.html index d9a92a1de..cefe970fe 100755 --- a/httemplate/edit/process/cust_pay-pkgnum.html +++ b/httemplate/edit/process/cust_pay-pkgnum.html @@ -4,7 +4,7 @@ %} else { <% header(emt('Payment package changed')) %> diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi index 9e5f3d3f7..15b26f9c6 100755 --- a/httemplate/edit/process/cust_pay.cgi +++ b/httemplate/edit/process/cust_pay.cgi @@ -14,7 +14,7 @@ % <% header(emt('Payment entered')) %> diff --git a/httemplate/edit/process/cust_pay_pending.html b/httemplate/edit/process/cust_pay_pending.html index 1bad6cffe..80bd14aaf 100644 --- a/httemplate/edit/process/cust_pay_pending.html +++ b/httemplate/edit/process/cust_pay_pending.html @@ -3,7 +3,7 @@ Error: <% $error |h %> % } else { % } @@ -59,6 +59,15 @@ if ( $action eq 'delete' ) { $title = 'Pending payment completed (decline)'; } +} elsif ( $action eq 'reverse' ) { + + $error = $cust_pay_pending->reverse; + if ( $error ) { + $title = 'Error reversing pending payment'; + } else { + $title = 'Pending payment completed (reverse)'; + } + } else { die "unknown action $action"; diff --git a/httemplate/edit/process/cust_pkg_detail.html b/httemplate/edit/process/cust_pkg_detail.html index 132ff63c5..25fabd930 100644 --- a/httemplate/edit/process/cust_pkg_detail.html +++ b/httemplate/edit/process/cust_pkg_detail.html @@ -6,7 +6,7 @@ % } else { <% header($action) %> % } diff --git a/httemplate/edit/process/cust_pkg_discount.html b/httemplate/edit/process/cust_pkg_discount.html index 143611ef9..963546363 100644 --- a/httemplate/edit/process/cust_pkg_discount.html +++ b/httemplate/edit/process/cust_pkg_discount.html @@ -5,7 +5,7 @@ <% header("Discount applied") %> diff --git a/httemplate/edit/process/cust_pkg_quantity.html b/httemplate/edit/process/cust_pkg_quantity.html index fb2657252..b60595583 100644 --- a/httemplate/edit/process/cust_pkg_quantity.html +++ b/httemplate/edit/process/cust_pkg_quantity.html @@ -5,7 +5,7 @@ <& /elements/header-popup.html, "Quantity changed" &> diff --git a/httemplate/edit/process/cust_pkg_salesnum.html b/httemplate/edit/process/cust_pkg_salesnum.html index aab37416a..c1cb26813 100644 --- a/httemplate/edit/process/cust_pkg_salesnum.html +++ b/httemplate/edit/process/cust_pkg_salesnum.html @@ -5,7 +5,7 @@ <& /elements/header-popup.html, "Sales Person changed" &> diff --git a/httemplate/edit/process/cust_refund.cgi b/httemplate/edit/process/cust_refund.cgi index 8977ced20..d4236bcdf 100755 --- a/httemplate/edit/process/cust_refund.cgi +++ b/httemplate/edit/process/cust_refund.cgi @@ -7,7 +7,7 @@ % <% header('Refund entered') %> diff --git a/httemplate/edit/process/cust_tax_adjustment.html b/httemplate/edit/process/cust_tax_adjustment.html index 204b5b9f7..fe232757f 100644 --- a/httemplate/edit/process/cust_tax_adjustment.html +++ b/httemplate/edit/process/cust_tax_adjustment.html @@ -4,7 +4,7 @@ % } else { <% header("Tax adjustment added") %> diff --git a/httemplate/edit/process/detach-cust_pkg.html b/httemplate/edit/process/detach-cust_pkg.html index 782ffa5e0..34c580560 100644 --- a/httemplate/edit/process/detach-cust_pkg.html +++ b/httemplate/edit/process/detach-cust_pkg.html @@ -5,7 +5,7 @@ <% header(emt("Package detached")) %> diff --git a/httemplate/edit/process/domain_record.cgi b/httemplate/edit/process/domain_record.cgi index 8369f7114..9d869d547 100755 --- a/httemplate/edit/process/domain_record.cgi +++ b/httemplate/edit/process/domain_record.cgi @@ -3,7 +3,7 @@ %} elsif ( $recnum ) { #editing <% header('Nameservice record changed') %> %} else { #adding diff --git a/httemplate/edit/process/elements/ApplicationCommon.html b/httemplate/edit/process/elements/ApplicationCommon.html index a73b1bccd..67fa89196 100644 --- a/httemplate/edit/process/elements/ApplicationCommon.html +++ b/httemplate/edit/process/elements/ApplicationCommon.html @@ -26,7 +26,7 @@ Examples: %} else { <% header("$src_thing application$to sucessful") %> diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index 60aaf749a..76722c960 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -188,7 +188,7 @@ process(); <% include('/elements/header-popup.html', $opt{'popup_reload'} ) %> diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi index d1b8e1086..00d17c8b1 100644 --- a/httemplate/edit/process/quick-charge.cgi +++ b/httemplate/edit/process/quick-charge.cgi @@ -1,4 +1,13 @@ +% if ( $error ) { +% $cgi->param('error', $error ); <% $cgi->redirect($redirect) %> +% } else { +<% header(emt($message)) %> + + +% } <%init> my $curuser = $FS::CurrentUser::CurrentUser; diff --git a/httemplate/edit/process/quotation_pkg_detail.html b/httemplate/edit/process/quotation_pkg_detail.html index b836baebc..5728832b2 100644 --- a/httemplate/edit/process/quotation_pkg_detail.html +++ b/httemplate/edit/process/quotation_pkg_detail.html @@ -6,7 +6,7 @@ % } else { <% header($action) %> % } diff --git a/httemplate/elements/commission_rate.html b/httemplate/elements/commission_rate.html new file mode 100644 index 000000000..071ebb1e3 --- /dev/null +++ b/httemplate/elements/commission_rate.html @@ -0,0 +1,68 @@ +% unless ( $opt{'js_only'} ) { + + + + <& select.html, + field => "${name}_cycle", + options => [ '', 1 .. 12 ], + option_labels => { + '' => '', + 1 => '1st', + 2 => '2nd', + 3 => '3rd', + map { $_ => $_.'th' } 4 .. 12 + }, + onchange => $onchange, + curr_value => $commission_rate->get("cycle"), + &> + <% $money_char %> + <& input-text.html, + field => "${name}_amount", + size => 8, + curr_value => $commission_rate->get("amount") + || '0.00', + 'text-align' => 'right' + &> + + + <& input-text.html, + field => "${name}_percent", + size => 8, + curr_value => $commission_rate->get("percent") + || '0', + 'text-align' => 'right' + &>% +% } +<%init> + +my( %opt ) = @_; + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; + +my $name = $opt{'field'} || 'commissionratenum'; +my $id = $opt{'id'} || 'commissionratenum'; + +my $curr_value = $opt{'curr_value'} || $opt{'value'}; + +my $onchange = ''; +if ( $opt{'onchange'} ) { + $onchange = $opt{'onchange'}; + $onchange .= '(this)' unless $onchange =~ /\(\w*\);?$/; + $onchange =~ s/\(what\);/\(this\);/g; #ugh, terrible hack. all onchange + #callbacks should act the same + $onchange = 'onChange="'. $onchange. '"'; +} + +my $commission_rate; +if ( $curr_value ) { + $commission_rate = qsearchs('commission_rate', { 'commissionratenum' => $curr_value } ); +} else { + $commission_rate = new FS::commission_rate {}; +} + +foreach my $field (qw( amount percent cycle)) { + my $value = $cgi->param("${name}_${field}"); + $commission_rate->set($field, $value) if $value; +} + + diff --git a/httemplate/elements/header-full.html b/httemplate/elements/header-full.html index 07595a539..850eaed8c 100644 --- a/httemplate/elements/header-full.html +++ b/httemplate/elements/header-full.html @@ -53,7 +53,7 @@ Example: % } <% include('init_overlib.html') |n %> <% include('rs_init_object.html') |n %> - + <% $head |n %> %# announce our base path, and the Mason comp path of this page diff --git a/httemplate/elements/header-popup.html b/httemplate/elements/header-popup.html index 6c0f80b37..839a63676 100644 --- a/httemplate/elements/header-popup.html +++ b/httemplate/elements/header-popup.html @@ -34,6 +34,7 @@ Example: % } % } + <% $head |n %> > diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 0f98bc960..88c1df3c8 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -672,7 +672,10 @@ $config_cust{'Note classes'} = [ $fsurl.'browse/cust_note_class.html', 'Note cla tie my %config_agent, 'Tie::IxHash', 'Agent types' => [ $fsurl.'browse/agent_type.cgi', 'Agent types define groups of package definitions that you can then assign to particular agents' ], 'Agents' => [ $fsurl.'browse/agent.cgi', 'Agents are resellers of your service. Agents may be limited to a subset of your full offerings (via their type)' ], - 'Agent payment gateways' => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors for agent overrides' ]; + 'Agent payment gateways' => [ $fsurl.'browse/payment_gateway.html', 'Credit card and electronic check processors for agent overrides' ], + 'separator' => '', + 'Commission schedules' => [ $fsurl.'browse/commission_schedule.html', + 'Commission schedules for consecutive billing periods' ], ; tie my %config_sales, 'Tie::IxHash', diff --git a/httemplate/elements/selectlayersx.html b/httemplate/elements/selectlayersx.html new file mode 100644 index 000000000..41f3cb0b7 --- /dev/null +++ b/httemplate/elements/selectlayersx.html @@ -0,0 +1,248 @@ +<%doc> + +Example: + + include( '/elements/selectlayers.html', + 'field' => $key, # SELECT element NAME (passed as form field) + # also used as ID and a unique key for layers and + # functions + 'curr_value' => $selected_layer, + 'options' => [ 'option1', 'option2' ], + 'labels' => { 'option1' => 'Option 1 Label', + 'option2' => 'Option 2 Label', + }, + + #XXX put this handling it its own selectlayers-fields.html element? + 'layer_prefix' => 'prefix_', #optional prefix for fieldnames + 'layer_fields' => { 'layer' => [ 'fieldname', + { label => 'fieldname2', + type => 'text', #implemented: + # text, money, fixed, + # hidden, checkbox, + # checkbox-multiple, + # select, select-agent, + # select-pkg_class, + # select-part_referral, + # select-taxclass, + # select-table, + #XXX tbd: + # more? + }, + ... + ], + 'layer2' => [ 'l2fieldname', + ... + ], + }, + + #current values for layer fields above + 'layer_values' => { 'layer' => { 'fieldname' => 'current_value', + 'fieldname2' => 'field2value', + ... + }, + 'layer2' => { 'l2fieldname' => 'l2value', + ... + }, + ... + }, + + #or manual control, instead of layer_fields and layer_values above + #called with args: my( $layer, $layer_fields, $layer_values, $layer_prefix ) + 'layer_callback' => + + 'html_between => '', #optional HTML displayed between the SELECT and the + #layers, scalar or coderef ('field' passed as a param) + 'onchange' => '', #javascript code run when the SELECT changes + # ("what" is the element) + 'js_only' => 0, #set true to return only the JS portions + 'html_only' => 0, #set true to return only the HTML portions + 'select_only' => 0, #set true to return only the + +% foreach my $option ( keys %$options ) { + + + +% } + + + +% } +% unless ( grep $opt{$_}, qw(js_only select_only layers_only) ) { + +<% ref($between) ? &{$between}($key) : $between %> + +% } +% +% unless ( grep $opt{$_}, qw(js_only select_only) ) { + +% foreach my $layer ( @layers ) { +% my $selected_layer; +% $selected_layer = $selected; + +
+ + <% &{$layer_callback}($layer, $layer_fields, $layer_values, $layer_prefix) %> + +
+ +% } + +% } +<%once> + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; +my $date_noinit = 0; + + +<%shared> + +my $selectlayersx_init = 0; + + +<%init> + +my %opt = @_; + +#use Data::Dumper; +#warn Dumper(%opt); + +my $key = $opt{field}; # || 'generate_one' #? + +tie my %options, 'Tie::IxHash', + map { $_ => $opt{'labels'}->{$_} } + @{ $opt{'options'} }; #just arrayref for now + +my $between = exists($opt{html_between}) ? $opt{html_between} : ''; +my $options = \%options; + +my @layers = (); +@layers = keys %options; + +my $selected = exists($opt{curr_value}) ? $opt{curr_value} : ''; + +#XXX eek. also eek $layer_fields in the layer_callback() call... +my $layer_fields = $opt{layer_fields}; +my $layer_values = $opt{layer_values}; +my $layer_prefix = $opt{layer_prefix}; + +my $layer_callback = $opt{layer_callback} || \&layer_callback; + +sub layer_callback { + my( $layer, $layer_fields, $layer_values, $layer_prefix ) = @_; + + return '' unless $layer && exists $layer_fields->{$layer}; + tie my %fields, 'Tie::IxHash', @{ $layer_fields->{$layer} }; + + #XXX this should become an element itself... (false laziness w/edit.html) + # but at least all the elements inside are the shared mason elements now + + return '' unless keys %fields; + my $html = ""; + + foreach my $field ( keys %fields ) { + + my $lf = ref($fields{$field}) + ? $fields{$field} + : { 'label'=>$fields{$field} }; + + my $value = $layer_values->{$layer}{$field}; + + my $type = $lf->{type} || 'text'; + + my $include = $type; + + if ( $include eq 'date' ) { + # several important differences from other tr-* + $html .= include( '/elements/tr-input-date-field.html', + { + 'name' => "$layer_prefix$field", + 'value' => $value, + 'label' => $lf->{label}, + 'format'=> $lf->{format}, + 'noinit'=> $date_noinit, + } + ); + $date_noinit = 1; + } + else { + $include = "input-$include" if $include =~ /^(text|money|percentage)$/; + $include = "tr-$include" unless $include eq 'hidden'; + $html .= include( "/elements/$include.html", + %$lf, + 'field' => "$layer_prefix$field", + 'id' => "$layer_prefix$field", #separate? + #don't want field0_label0...? + 'label_id' => $layer_prefix.$field."_label", + + 'value' => ( $lf->{'value'} || $value ), #hmm. + 'curr_value' => $value, + ); + } + } #foreach $field + $html .= '
'; + return $html; +} + + diff --git a/httemplate/elements/topreload.js b/httemplate/elements/topreload.js new file mode 100644 index 000000000..a66703b29 --- /dev/null +++ b/httemplate/elements/topreload.js @@ -0,0 +1,5 @@ +window.topreload = function() { + if (window != window.top) { + window.top.location.reload(); + } +} diff --git a/httemplate/elements/tr-select-reason.html b/httemplate/elements/tr-select-reason.html index 97466f175..9a430222c 100755 --- a/httemplate/elements/tr-select-reason.html +++ b/httemplate/elements/tr-select-reason.html @@ -188,9 +188,8 @@ my $class = $opt{'reason_class'}; my $init_reason; if ( $opt{'cgi'} ) { $init_reason = $opt{'cgi'}->param($name); -} else { - $init_reason = $opt{'curr_value'}; } +$init_reason ||= $opt{'curr_value'}; my $id = $opt{'id'} || $name; $id =~ s/\./_/g; # for edit/part_event diff --git a/httemplate/elements/tr-selectlayersx.html b/httemplate/elements/tr-selectlayersx.html new file mode 100644 index 000000000..ca7a36079 --- /dev/null +++ b/httemplate/elements/tr-selectlayersx.html @@ -0,0 +1,25 @@ +% unless ( $opt{js_only} ) { + + <% include('tr-td-label.html', @_ ) %> + + > + +% } + + <% include('selectlayersx.html', @_ ) %> + +% unless ( $opt{js_only} ) { + + + + + +% } + +<%init> + +my %opt = @_; + +my $style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : ''; + + diff --git a/httemplate/misc/change_pkg_date.html b/httemplate/misc/change_pkg_date.html new file mode 100755 index 000000000..642a5b89e --- /dev/null +++ b/httemplate/misc/change_pkg_date.html @@ -0,0 +1,112 @@ +<& /elements/header-popup.html, mt($title) &> + +<& /elements/error.html &> + +% # only slightly different from unhold_pkg. +
+ + + +
+<% emt(($isstart ? 'Start billing' : 'Set contract end for').' [_1]', $part_pkg->pkg_comment(cust_pkg => $cust_pkg)) %> +
    +
  • + <& /elements/radio.html, + field => 'when', + id => 'when_now', + value => 'now', + curr_value => $when, + &> + +
  • +% if ( $next_bill_date ) { +
  • + <& /elements/radio.html, + field => 'when', + id => 'when_next_bill_date', + value => 'next_bill_date', + curr_value => $when, + &> + +
  • +% } +
  • +<& /elements/radio.html, + field => 'when', + id => 'when_date', + value => 'date', + curr_value => $when, +&> + +<& /elements/input-date-field.html, + { name => 'date_value', + value => $cgi->param('date_value') || $cust_pkg->get($field), + } +&> +
  • +
+ + +
+ + + +<%init> + +my $field = $cgi->param('field'); + +my ($acl, $isstart); +if ($field eq 'start_date') { + $acl = 'Change package start date'; + $isstart = 1; +} elsif ($field eq 'contract_end') { + $acl = 'Change package contract end date'; +} else { + die "Unknown date field"; +} + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right($acl); + +my $pkgnum; +if ( $cgi->param('pkgnum') =~ /^(\d+)$/ ) { + $pkgnum = $1; +} else { + die "illegal query ". $cgi->keywords; +} + +my $conf = new FS::Conf; +my $date_format = $conf->config('date_format') || '%m/%d/%Y'; + +my $title = $isstart ? 'Start billing package' : 'Change contract end'; + +my $cust_pkg = qsearchs({ + table => 'cust_pkg', + addl_from => ' JOIN cust_main USING (custnum) ', + hashref => { 'pkgnum' => $pkgnum }, + extra_sql => ' AND '. $curuser->agentnums_sql, +}) or die "Unknown pkgnum: $pkgnum"; + +my $next_bill_date = $cust_pkg->cust_main->next_bill_date; + +my $part_pkg = $cust_pkg->part_pkg; + +# defaults: +# sticky on error, then the existing date if any, then the customer's +# next bill date, and if none of those, default to now +my $when = $cgi->param('when'); + +if (!$when) { + if ($cust_pkg->get($field)) { + $when = 'date'; + } elsif ($next_bill_date) { + $when = 'next_bill_date'; + } else { + $when = 'now'; + } +} + diff --git a/httemplate/misc/change_pkg_start.html b/httemplate/misc/change_pkg_start.html deleted file mode 100755 index 5a890c86e..000000000 --- a/httemplate/misc/change_pkg_start.html +++ /dev/null @@ -1,99 +0,0 @@ -<& /elements/header-popup.html, mt($title) &> - -<& /elements/error.html &> - -% # only slightly different from unhold_pkg. -
- - -
-<% emt('Start billing [_1]', $part_pkg->pkg_comment(cust_pkg => $cust_pkg)) %> -
    -
  • - <& /elements/radio.html, - field => 'when', - id => 'when_now', - value => 'now', - curr_value => $when, - &> - -
  • -% if ( $next_bill_date ) { -
  • - <& /elements/radio.html, - field => 'when', - id => 'when_next_bill_date', - value => 'next_bill_date', - curr_value => $when, - &> - -
  • -% } -
  • -<& /elements/radio.html, - field => 'when', - id => 'when_date', - value => 'date', - curr_value => $when, -&> - -<& /elements/input-date-field.html, - { name => 'start_date', - value => $cgi->param('start_date') || $cust_pkg->start_date, - } -&> -
  • -
- - -
- - - -<%init> - -my $curuser = $FS::CurrentUser::CurrentUser; -die "access denied" - unless $curuser->access_right('Change package start date'); - -my $pkgnum; -if ( $cgi->param('pkgnum') =~ /^(\d+)$/ ) { - $pkgnum = $1; -} else { - die "illegal query ". $cgi->keywords; -} - -my $conf = new FS::Conf; -my $date_format = $conf->config('date_format') || '%m/%d/%Y'; - -my $title = 'Start billing package'; - -my $cust_pkg = qsearchs({ - table => 'cust_pkg', - addl_from => ' JOIN cust_main USING (custnum) ', - hashref => { 'pkgnum' => $pkgnum }, - extra_sql => ' AND '. $curuser->agentnums_sql, -}) or die "Unknown pkgnum: $pkgnum"; - -my $next_bill_date = $cust_pkg->cust_main->next_bill_date; - -my $part_pkg = $cust_pkg->part_pkg; - -# defaults: -# sticky on error, then the existing start date if any, then the customer's -# next bill date, and if none of those, default to now -my $when = $cgi->param('when'); - -if (!$when) { - if ($cust_pkg->start_date) { - $when = 'date'; - } elsif ($next_bill_date) { - $when = 'next_bill_date'; - } else { - $when = 'now'; - } -} - diff --git a/httemplate/misc/cust_main-cancel.cgi b/httemplate/misc/cust_main-cancel.cgi index f6fd1e915..73c4deb7a 100755 --- a/httemplate/misc/cust_main-cancel.cgi +++ b/httemplate/misc/cust_main-cancel.cgi @@ -1,6 +1,6 @@ <& /elements/header-popup.html, mt("Customer cancelled") &> diff --git a/httemplate/misc/cust_main-suspend.cgi b/httemplate/misc/cust_main-suspend.cgi index 7a501d61a..e81e2b490 100755 --- a/httemplate/misc/cust_main-suspend.cgi +++ b/httemplate/misc/cust_main-suspend.cgi @@ -1,6 +1,6 @@ <& /elements/header-popup.html, mt("Customer suspended") &> diff --git a/httemplate/misc/cust_main-unsuspend.cgi b/httemplate/misc/cust_main-unsuspend.cgi index e8ac8d31e..99ec70a23 100755 --- a/httemplate/misc/cust_main-unsuspend.cgi +++ b/httemplate/misc/cust_main-unsuspend.cgi @@ -1,6 +1,6 @@ <& /elements/header-popup.html, mt("Customer unsuspended") &> diff --git a/httemplate/misc/delete-addr_range.html b/httemplate/misc/delete-addr_range.html index c6310e9b1..239332d23 100644 --- a/httemplate/misc/delete-addr_range.html +++ b/httemplate/misc/delete-addr_range.html @@ -3,7 +3,7 @@ % } else { <& /elements/header-popup.html, "Address range deleted" &> diff --git a/httemplate/misc/delete-rate_detail.html b/httemplate/misc/delete-rate_detail.html index 30856a73a..b4d31b379 100755 --- a/httemplate/misc/delete-rate_detail.html +++ b/httemplate/misc/delete-rate_detail.html @@ -3,7 +3,7 @@ % } else { <% header('Rate deleted') %> % } diff --git a/httemplate/misc/did_order_confirmed.html b/httemplate/misc/did_order_confirmed.html index 53dbb2f3a..3cc121c6f 100644 --- a/httemplate/misc/did_order_confirmed.html +++ b/httemplate/misc/did_order_confirmed.html @@ -14,7 +14,7 @@ die "access denied" my $action = $1; my $header = ''; my $popup = ''; -my $js = 'window.top.location.reload();'; +my $js = 'topreload();'; $cgi->param('ordernum') =~ /^(\d+)$/ or die 'illegal ordernum'; my $ordernum = $1; diff --git a/httemplate/misc/disable-cust_location.cgi b/httemplate/misc/disable-cust_location.cgi index ee7ba1dbc..677f0b891 100755 --- a/httemplate/misc/disable-cust_location.cgi +++ b/httemplate/misc/disable-cust_location.cgi @@ -1,6 +1,6 @@ <% header("Location disabled") %> diff --git a/httemplate/misc/disable-msg_template.cgi b/httemplate/misc/disable-msg_template.cgi index 1eb4d25e5..565eb2cee 100644 --- a/httemplate/misc/disable-msg_template.cgi +++ b/httemplate/misc/disable-msg_template.cgi @@ -3,7 +3,7 @@ % } else { <& /elements/header-popup.html, "Template ${actioned}" &> diff --git a/httemplate/misc/process/bulk_pkg_increment_bill.cgi b/httemplate/misc/process/bulk_pkg_increment_bill.cgi index 0d8417b26..8da849508 100755 --- a/httemplate/misc/process/bulk_pkg_increment_bill.cgi +++ b/httemplate/misc/process/bulk_pkg_increment_bill.cgi @@ -4,7 +4,7 @@ %} else { <% header('Packages Adjusted') %> % } diff --git a/httemplate/misc/process/cancel_pkg.html b/httemplate/misc/process/cancel_pkg.html index eb3b2efe6..46ba06a6d 100755 --- a/httemplate/misc/process/cancel_pkg.html +++ b/httemplate/misc/process/cancel_pkg.html @@ -1,6 +1,6 @@ <% header(emt("Package $past_method")) %> diff --git a/httemplate/misc/process/change_pkg_contact.html b/httemplate/misc/process/change_pkg_contact.html index 2795c1197..5bf896200 100644 --- a/httemplate/misc/process/change_pkg_contact.html +++ b/httemplate/misc/process/change_pkg_contact.html @@ -1,6 +1,6 @@ <% header(emt("Package contact $past_method")) %> diff --git a/httemplate/misc/process/change_pkg_date.html b/httemplate/misc/process/change_pkg_date.html new file mode 100755 index 000000000..3084ec538 --- /dev/null +++ b/httemplate/misc/process/change_pkg_date.html @@ -0,0 +1,67 @@ +<& /elements/header-popup.html &> + + + +<%init> + +my $field = $cgi->param('field'); + +my ($acl, $isstart); +if ($field eq 'start_date') { + $acl = 'Change package start date'; + $isstart = 1; +} elsif ($field eq 'contract_end') { + $acl = 'Change package contract end date'; +} else { + die "Unknown date field"; +} + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" + unless $curuser->access_right($acl); + +$cgi->param('pkgnum') =~ /^(\d+)$/ + or die "illegal pkgnum"; +my $pkgnum = $1; + +my $cust_pkg = qsearchs({ + table => 'cust_pkg', + addl_from => ' JOIN cust_main USING (custnum) ', + hashref => { 'pkgnum' => $pkgnum }, + extra_sql => ' AND '. $curuser->agentnums_sql, +}) or die "Unknown pkgnum: $pkgnum"; + +my $cust_main = $cust_pkg->cust_main; + +my $error; +my $date_value; +if ( $cgi->param('when') eq 'now' ) { + # blank start means start it the next time billing runs ("Now") + # blank contract end means it never ends ("Never") + $date_value = ''; +} elsif ( $cgi->param('when') eq 'next_bill_date' ) { + $date_value = $cust_main->next_bill_date; +} elsif ( $cgi->param('when') eq 'date' ) { + $date_value = parse_datetime($cgi->param('date_value')); +} + +if ( $isstart && $cust_pkg->setup ) { + # shouldn't happen + $error = 'This package has already started billing.'; +} else { + local $FS::UID::AutoCommit = 0; + foreach my $pkg ($cust_pkg, $cust_pkg->supplemental_pkgs) { + last if $error; + $pkg->set($field, $date_value); + $error ||= $pkg->replace; + } + $error ? dbh->rollback : dbh->commit; +} + +if ( $error ) { + $cgi->param('error', $error); + print $cgi->redirect($fsurl.'misc/change_pkg_date.html?', $cgi->query_string); +} + diff --git a/httemplate/misc/process/change_pkg_start.html b/httemplate/misc/process/change_pkg_start.html deleted file mode 100755 index 17a8518f9..000000000 --- a/httemplate/misc/process/change_pkg_start.html +++ /dev/null @@ -1,53 +0,0 @@ -<& /elements/header-popup.html &> - - - -<%init> - -my $curuser = $FS::CurrentUser::CurrentUser; -die "access denied" - unless $curuser->access_right('Change package start date'); - -$cgi->param('pkgnum') =~ /^(\d+)$/ - or die "illegal pkgnum"; -my $pkgnum = $1; - -my $cust_pkg = qsearchs({ - table => 'cust_pkg', - addl_from => ' JOIN cust_main USING (custnum) ', - hashref => { 'pkgnum' => $pkgnum }, - extra_sql => ' AND '. $curuser->agentnums_sql, -}) or die "Unknown pkgnum: $pkgnum"; - -my $cust_main = $cust_pkg->cust_main; - -my $error; -my $start_date; -if ( $cgi->param('when') eq 'now' ) { - # start it the next time billing runs - $start_date = ''; -} elsif ( $cgi->param('when') eq 'next_bill_date' ) { - $start_date = $cust_main->next_bill_date; -} elsif ( $cgi->param('when') eq 'date' ) { - $start_date = parse_datetime($cgi->param('start_date')); -} - -if ( $cust_pkg->setup ) { - # shouldn't happen - $error = 'This package has already started billing.'; -} else { - local $FS::UID::AutoCommit = 0; - foreach my $pkg ($cust_pkg, $cust_pkg->supplemental_pkgs) { - $pkg->set('start_date', $start_date); - $error ||= $pkg->replace; - } - $error ? dbh->rollback : dbh->commit; -} - -if ( $error ) { - $cgi->param('error', $error); - print $cgi->redirect($fsurl.'misc/change_pkg_start.html?', $cgi->query_string); -} - diff --git a/httemplate/misc/process/cust_bill-promised_date.html b/httemplate/misc/process/cust_bill-promised_date.html index 721a763eb..f390609c7 100644 --- a/httemplate/misc/process/cust_bill-promised_date.html +++ b/httemplate/misc/process/cust_bill-promised_date.html @@ -1,4 +1,4 @@ - + <%init> # XXX ACL? die "access denied" diff --git a/httemplate/misc/process/delay_susp_pkg.html b/httemplate/misc/process/delay_susp_pkg.html index 675da0496..15a3c963c 100755 --- a/httemplate/misc/process/delay_susp_pkg.html +++ b/httemplate/misc/process/delay_susp_pkg.html @@ -1,6 +1,6 @@ <% header($msg) %> diff --git a/httemplate/misc/process/enable_or_disable_tax.html b/httemplate/misc/process/enable_or_disable_tax.html index 9b7324b0d..8a7a559a0 100755 --- a/httemplate/misc/process/enable_or_disable_tax.html +++ b/httemplate/misc/process/enable_or_disable_tax.html @@ -4,7 +4,7 @@ <% include('/elements/header-popup.html', $title) %> diff --git a/httemplate/misc/process/nms-add_iface.html b/httemplate/misc/process/nms-add_iface.html index 79e685686..cbd0fc048 100644 --- a/httemplate/misc/process/nms-add_iface.html +++ b/httemplate/misc/process/nms-add_iface.html @@ -1,6 +1,6 @@ <& /elements/header-popup.html, 'Interface added' &> <%init> diff --git a/httemplate/misc/process/nms-add_router.html b/httemplate/misc/process/nms-add_router.html index c3b42a8d3..a4c3423da 100644 --- a/httemplate/misc/process/nms-add_router.html +++ b/httemplate/misc/process/nms-add_router.html @@ -1,6 +1,6 @@ <& /elements/header-popup.html, 'Router added' &> <%init> diff --git a/httemplate/misc/process/recharge_svc.html b/httemplate/misc/process/recharge_svc.html index 2d49f6b06..88a1f7f36 100755 --- a/httemplate/misc/process/recharge_svc.html +++ b/httemplate/misc/process/recharge_svc.html @@ -4,7 +4,7 @@ %} else { <% header("Package recharged") %> %} diff --git a/httemplate/misc/process/unhold_pkg.html b/httemplate/misc/process/unhold_pkg.html index 694048023..7e54262c6 100755 --- a/httemplate/misc/process/unhold_pkg.html +++ b/httemplate/misc/process/unhold_pkg.html @@ -1,6 +1,6 @@ <& /elements/header-popup.html &> diff --git a/httemplate/misc/process/void-cust_bill.html b/httemplate/misc/process/void-cust_bill.html index 7773b0ba9..32a2fc591 100755 --- a/httemplate/misc/process/void-cust_bill.html +++ b/httemplate/misc/process/void-cust_bill.html @@ -4,7 +4,7 @@ %} else { <& /elements/header-popup.html, 'Invoice voided' &> %} diff --git a/httemplate/misc/reason-merge.html b/httemplate/misc/reason-merge.html index 14f5ebb84..b7e4df454 100644 --- a/httemplate/misc/reason-merge.html +++ b/httemplate/misc/reason-merge.html @@ -1,7 +1,7 @@ % if ($success) { <% include('/elements/header-popup.html', 'Reason Merge Success') %> % } else { <% include('/elements/header-popup.html', 'Merge Reasons') %> diff --git a/httemplate/misc/void-cust_credit.html b/httemplate/misc/void-cust_credit.html index 1e71f0030..81ba31d54 100755 --- a/httemplate/misc/void-cust_credit.html +++ b/httemplate/misc/void-cust_credit.html @@ -1,7 +1,7 @@ %if ( $success ) { <& /elements/header-popup.html, mt("Credit voided") &> diff --git a/httemplate/search/cust_bill_pay_pkg.html b/httemplate/search/cust_bill_pay_pkg.html index 7c231a65d..e2ffd1258 100644 --- a/httemplate/search/cust_bill_pay_pkg.html +++ b/httemplate/search/cust_bill_pay_pkg.html @@ -14,7 +14,7 @@ #payment 'Date', - 'Order Number', + @on_header, 'By', #application @@ -44,7 +44,7 @@ ? cardtype($cust_pay->paymask) : ''; }, sub { time2str('%b %d %Y', shift->get('cust_pay_date') ) }, - sub { shift->cust_bill_pay->cust_pay->order_number }, + @on_field, sub { shift->cust_bill_pay->cust_pay->otaker }, sub { sprintf($money_char.'%.2f', shift->amount ) }, @@ -66,7 +66,7 @@ '', #payinfo/paymask '', #cardtype 'cust_pay_date', - '', #order_number + @on_null, #order_number '', #'otaker', '', #amount '', #line item description @@ -83,7 +83,7 @@ '', '', '', - '', + @on_null, '', '', '', @@ -96,10 +96,9 @@ FS::UI::Web::cust_header() ), ], - 'align' => 'rcrlrrlrlll', -#original value before cardtype & package were added -#why are there 13 cols? -#'rcrrlrlllrrcl'. + 'align' => 'rcrlr'. + $on_align. + 'lrlll'. $post_desc_align. 'rr'. FS::UI::Web::cust_aligns(), @@ -109,7 +108,7 @@ '', '', '', - '', + @on_null, '', '', '', @@ -126,7 +125,7 @@ '', '', '', - '', + @on_null, '', '', '', @@ -148,6 +147,17 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Financial reports'); +my @on_header = (); +my @on_field = (); +my @on_null = (); +my $on_align = ''; +if ($cgi->param('show_order_number')) { + @on_header = ('Order Number'); + @on_field = (sub { shift->cust_bill_pay->cust_pay->order_number }); + @on_null = (''); + $on_align = 'r'; +} + my $conf = new FS::Conf; my %payby = FS::payby->payby2shortname; diff --git a/httemplate/search/cust_pay.html b/httemplate/search/cust_pay.html index 536ab291f..e466f6afa 100755 --- a/httemplate/search/cust_pay.html +++ b/httemplate/search/cust_pay.html @@ -4,5 +4,4 @@ 'name_singular' => emt('payment'), 'name_verb' => emt('paid'), 'show_card_type' => 1, - 'show_order_number' => 1, &> diff --git a/httemplate/search/cust_pay_pending.html b/httemplate/search/cust_pay_pending.html index 8662d1989..697bdbbf0 100755 --- a/httemplate/search/cust_pay_pending.html +++ b/httemplate/search/cust_pay_pending.html @@ -17,7 +17,7 @@ my %statusaction = ( 'new' => 'delete', 'pending' => 'complete', - #'authorized' => '', + 'authorized' => 'complete', 'captured' => 'capture', #'declined' => '', #wouldn't need to take action on a done state#'done' diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index 03aaedd36..1b1be5f36 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -74,6 +74,7 @@ my %cardtype_of = ( 'Amex' => q['American Express card'], 'Discover' => q['Discover card'], 'Maestro' => q['Switch', 'Solo', 'Laser'], + 'Tokenized' => q['Tokenized'], ); <%init> @@ -100,29 +101,30 @@ my $title = ''; $title = 'Unapplied ' if $unapplied; $title .= "\u$name_singular Search Results"; -my $link = ''; -if ( ( $curuser->access_right('View invoices') #remove in 2.5 (2.7?) - || ($curuser->access_right('View payments') && $table =~ /^cust_pay/) - || ($curuser->access_right('View refunds') && $table eq 'cust_refund') - ) - && ! $opt{'disable_link'} - ) -{ - - my $key; - my $q = ''; - if ( $table eq 'cust_pay_void' ) { - $key = 'paynum'; - $q .= 'void=1;'; - } elsif ( $table eq /^cust_(\w+)$/ ) { - $key = $1.'num'; - } - - if ( $key ) { - $q .= "$key="; - $link = [ "${p}view/$table.html?$q", $key ] - } -} +###NOT USED??? +#my $link = ''; +#if ( ( $curuser->access_right('View invoices') #remove in 2.5 (2.7?) +# || ($curuser->access_right('View payments') && $table =~ /^cust_pay/) +# || ($curuser->access_right('View refunds') && $table eq 'cust_refund') +# ) +# && ! $opt{'disable_link'} +# ) +#{ +# +# my $key; +# my $q = ''; +# if ( $table eq 'cust_pay_void' ) { +# $key = 'paynum'; +# $q .= 'void=1;'; +# } elsif ( $table eq /^cust_(\w+)$/ ) { +# $key = $1.'num'; +# } +# +# if ( $key ) { +# $q .= "$key="; +# $link = [ "${p}view/$table.html?$q", $key ] +# } +#} my $cust_link = sub { my $cust_thing = shift; @@ -175,12 +177,18 @@ if ( $opt{'pre_header'} ) { push @sort_fields, @{ $opt{'pre_fields'} }; } -my $sub_receipt = sub { +my $sub_receipt = $opt{'disable_link'} ? '' : sub { my $obj = shift; my $objnum = $obj->primary_key . '=' . $obj->get($obj->primary_key); + my $table = $obj->table; + my $void = ''; + if ($table eq 'cust_pay_void') { + $table = 'cust_pay'; + $void = ';void=1'; + } include('/elements/popup_link_onclick.html', - 'action' => $p.'view/cust_pay.html?link=popup;'.$objnum, + 'action' => $p.'view/'.$table.'.html?link=popup;'.$objnum.$void, 'actionlabel' => emt('Payment Receipt'), ); }; @@ -218,7 +226,7 @@ push @links, ''; push @fields, sub { time2str('%b %d %Y', shift->_date ) }; push @sort_fields, '_date'; -if ($opt{'show_order_number'}) { +if ($cgi->param('show_order_number')) { push @header, emt('Order Number'); $align .= 'r'; push @links, ''; @@ -326,18 +334,9 @@ if ( $cgi->param('magic') ) { if ( $subtype ) { - if ( $subtype eq 'Tokenized' ) { - - $payby_search .= " AND substring($table.payinfo from 1 for 2 ) = '99' "; - # XXX should store the cardtype as 'Tokenized' in this case? - - } else { - - my $in_cardtype = $cardtype_of{$subtype} - or die "unknown card type $subtype"; - $payby_search .= " AND $table.paycardtype IN($in_cardtype)"; - - } + my $in_cardtype = $cardtype_of{$subtype} + or die "unknown card type $subtype"; + $payby_search .= " AND $table.paycardtype IN($in_cardtype)"; } @@ -499,8 +498,6 @@ if ( $cgi->param('magic') ) { 'addl_from' => $addl_from, }; -warn Dumper \$sql_query; - } else { #hmm... is this still used? diff --git a/httemplate/search/elements/report_cust_pay_or_refund.html b/httemplate/search/elements/report_cust_pay_or_refund.html index 730db68e8..806746a23 100644 --- a/httemplate/search/elements/report_cust_pay_or_refund.html +++ b/httemplate/search/elements/report_cust_pay_or_refund.html @@ -151,6 +151,12 @@ Examples: 'value' => 1, &> + <& /elements/tr-checkbox.html, + 'label' => emt('Include order number'), + 'field' => 'show_order_number', + 'value' => 1, + &> + % } diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html index a279f5327..b6ee7b373 100644 --- a/httemplate/search/elements/search.html +++ b/httemplate/search/elements/search.html @@ -135,8 +135,11 @@ Example: # sort, link & display properties for fields - 'sort_fields' => [], #optional list of field names or SQL expressions for - # sorts + 'sort_fields' => [], #optional list of field names or SQL expressions for sorts + + 'order_by_sql' => { #to keep complex SQL expressions out of cgi order_by value, + 'fieldname' => 'sql snippet', # maps fields/sort_fields values to sql snippets + } #listref - each item is the empty string, # or a listref of link and method name to append, @@ -406,6 +409,12 @@ $order_by = $cgi->param('order_by') if $cgi->param('order_by'); my $header = [ map { ref($_) ? $_->{'label'} : $_ } @{$opt{header}} ]; my $rows; +my ($order_by_key,$order_by_desc) = ($order_by =~ /^\s*(.*?)(\s+DESC)?\s*$/i); +$opt{'order_by_sql'} ||= {}; +$order_by_desc ||= ''; +$order_by = $opt{'order_by_sql'}{$order_by_key} . $order_by_desc + if $opt{'order_by_sql'}{$order_by_key}; + if ( ref $query ) { my @query; if (ref($query) eq 'HASH') { diff --git a/httemplate/search/log.html b/httemplate/search/log.html index 111200f55..5b330f899 100644 --- a/httemplate/search/log.html +++ b/httemplate/search/log.html @@ -81,15 +81,15 @@ a:visited {text-decoration: none} Level <& /elements/select.html, field => 'min_level', - options => [ 0..7 ], - labels => { map {$_ => $FS::Log::LEVELS[$_]} 0..7 }, + options => [ &FS::Log::levelnums ], + labels => { &FS::Log::levelmap }, curr_value => $cgi->param('min_level'), &> to <& /elements/select.html, field => 'max_level', - options => [ 0..7 ], - labels => { map {$_ => $FS::Log::LEVELS[$_]} 0..7 }, + options => [ &FS::Log::levelnums ], + labels => { &FS::Log::levelmap }, curr_value => $cgi->param('max_level'), &> @@ -128,7 +128,7 @@ a:visited {text-decoration: none} <%once> my $date_sub = sub { time2str('%Y-%m-%d %T', $_[0]->_date) }; -my $level_sub = sub { $FS::Log::LEVELS[$_[0]->level] }; +my $level_sub = sub { $FS::Log::LEVELS{$_[0]->level} }; my $context_sub = sub { my $log = shift; @@ -191,18 +191,15 @@ my $object_link_sub = sub { } }; -my @colors = ( - '404040', #debug - '0000aa', #info - '00aa00', #notice - 'aa0066', #warning - '000000', #error - 'aa0000', #critical - 'ff0000', #alert - 'ff0000', #emergency +my %colors = ( + 0 => '404040', #debug, gray + 1 => '000000', #info, black + 3 => '0000aa', #warning, blue + 4 => 'aa0066', #error, purple + 5 => 'ff0000', #critical, red ); -my $color_sub = sub { $colors[ $_[0]->level ]; }; +my $color_sub = sub { $colors{ $_[0]->level }; }; my @contexts = ('', sort FS::log_context->contexts); @@ -212,10 +209,10 @@ die "access denied" unless $curuser->access_right([ 'View system logs', 'Configuration' ]); my @menubar = (); -push @menubar, qq(Configure conditions for sending email when logging), +push @menubar, qq(Configure conditions for sending email when logging); $cgi->param('min_level', 0) unless defined($cgi->param('min_level')); -$cgi->param('max_level', 7) unless defined($cgi->param('max_level')); +$cgi->param('max_level', 5) unless defined($cgi->param('max_level')); my %search = (); $search{'date'} = [ FS::UI::Web::parse_beginning_ending($cgi) ]; diff --git a/httemplate/search/report_cust_bill_pay_pkg.html b/httemplate/search/report_cust_bill_pay_pkg.html index 2347bab6d..bdcd1549e 100644 --- a/httemplate/search/report_cust_bill_pay_pkg.html +++ b/httemplate/search/report_cust_bill_pay_pkg.html @@ -41,6 +41,13 @@ field => 'paid', &> + <& /elements/tr-checkbox.html, + 'label' => emt('Display order number'), + 'field' => 'show_order_number', + 'value' => 1, + 'cell_style' => 'font-weight: normal', #for consistency + &> +