}
+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;
'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',
# },
{
+ '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*.',
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 => @_ );
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;
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 ) {
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
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 (
}
+=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
'commission_agentnum', 'int', 'NULL', '', '', '', #
'commission_salesnum', 'int', 'NULL', '', '', '', #
'commission_pkgnum', 'int', 'NULL', '', '', '', #
+ 'commission_invnum', 'int', 'NULL', '', '', '',
'credbatch', 'varchar', 'NULL', $char_d, '', '',
],
'primary_key' => 'crednum',
table => 'cust_pkg',
references => [ 'pkgnum' ],
},
+ { columns => [ 'commission_invnum' ],
+ table => 'cust_bill',
+ references => [ 'invnum' ],
+ },
],
},
'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, '', '',
table => 'cust_pkg',
references => [ 'pkgnum' ],
},
+ { columns => [ 'commission_invnum' ],
+ table => 'cust_bill',
+ references => [ 'invnum' ],
+ },
{ columns => [ 'void_reasonnum' ],
table => 'reason',
references => [ 'reasonnum' ],
],
},
+ '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' => {
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;
tie my %hash, 'Tie::IxHash',
+ #remap log levels
+ 'log' => [],
+
#cust_main (remove paycvv from history, locations, cust_payby, etc)
'cust_main' => [],
#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;
'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',
);
--- /dev/null
+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_schedule>.
+
+FS::commission_rate inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item commissionratenum - primary key
+
+=item schedulenum - L<FS::commission_schedule> 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<FS::Record>
+
+=cut
+
+1;
+
--- /dev/null
+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<FS::reason>) 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<FS::part_event> 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<FS::cust_bill> 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<FS::Record>, L<FS::part_event>, L<FS::commission_rate>
+
+=cut
+
+1;
+
} #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
|| $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;
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 );
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
# 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 $_";
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')
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
\%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',
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') {
# 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
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;
# 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
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')
}
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
'';
}
$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.
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;
use FS::UID qw( dbh driver_name );
use FS::log_context;
use FS::log_email;
+use FS::upgrade_journal;
+use Tie::IxHash;
=head1 NAME
'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,
},
};
}
+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
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
upgrade_taxable_billpkgnum
freeside-paymentech-upload
freeside-paymentech-download
+ test
) );
=head1 NAME
###
$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
--- /dev/null
+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;
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) {
}
#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
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
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;
$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
# 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 '.
} )
) {
- 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
)
|| $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
;
}
=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
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_rate;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_schedule;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+#!/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;
--- /dev/null
+#!/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;
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,
'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',
If there is an error, returns a hash reference with a single key, B<error>,
otherwise returns a hash reference with a single key, B<custpaybynum>.
+=item update_payby HASHREF
+
+Updates stored payment information. Takes a hash reference with the same
+keys as insert_payby, as well as B<custpaybynum> to specify which record
+to update. All keys except B<session_id> and B<custpaybynum> 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<error>,
+otherwise returns a hash reference with a single key, B<custpaybynum>.
+
=item delete_payby HASHREF
Removes stored payment information. Takes a hash reference with two keys,
--- /dev/null
+<& 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' ];
+
+</%init>
],
'fields' => [ 'logemailnum',
sub { $_[0]->context || '(all)' },
- sub { $FS::Log::LEVELS[$_[0]->min_level] },
+ sub { $FS::Log::LEVELS{$_[0]->min_level} },
'msgname',
'to_addr',
$actions,
--- /dev/null
+<& 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;
+};
+
+</%init>
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};
<CENTER><FONT SIZE="+1"><B>Are you sure you want to delete this pending payment?</B></FONT></CENTER>
+% } elsif (( $action eq 'complete' ) and $authorized) {
+
+ <CENTER><FONT SIZE="+1"><B>Payment was authorized but not captured. Contact <% $cust_pay_pending->processor || 'the payment gateway' %> to establish the final disposition of this transaction.</B></FONT></CENTER>
+
% } elsif ( $action eq 'complete' ) {
<CENTER><FONT SIZE="+1"><B>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.</B></FONT></CENTER>
% } else {
-%# if ( $action eq 'complete' ) {
-
<INPUT TYPE="hidden" NAME="action" VALUE="">
<TR>
<BUTTON TYPE="button" onClick="document.pendingform.action.value = 'insert_cust_pay'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/tick.png" ALT=""-->Yes, transaction completed sucessfully.</BUTTON>
</TD>
-% if ( $action eq 'complete' ) {
+% if ( $action eq 'complete' ) {
<TD> </TD>
+% if ($authorized) {
+ <TD ALIGN="center">
+ <BUTTON TYPE="button" onClick="document.pendingform.action.value = 'reverse'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, transaction was reversed</BUTTON>
+ </TD>
+% } else {
<TD ALIGN="center">
<BUTTON TYPE="button" onClick="document.pendingform.action.value = 'decline'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, transaction was declined</BUTTON>
</TD>
+% }
<TD> </TD>
<TD ALIGN="center">
<BUTTON TYPE="button" onClick="document.pendingform.action.value = 'delete'; document.pendingform.submit();"><!--IMG SRC="<%$p%>images/cross.png" ALT=""-->No, transaction was not received</BUTTON>
</TD>
- </TR>
% }
+ </TR>
+
<TR><TD COLSPAN=5></TD></TR>
<TR>
})
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') || '$';
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 = '';
},
{ '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',
value => 'Event Conditions',
},
{ field => 'conditionname',
- type => 'selectlayers',
+ type => 'selectlayersx',
options => [ keys %all_conditions ],
labels => \%condition_labels,
onchange => 'condition_changed(what);',
value => 'Event Action',
},
{ field => 'action',
- type => 'selectlayers',
+ type => 'selectlayersx',
options => [ keys %all_actions ],
labels => \%action_labels,
onchange => 'action_changed(what);',
<% include('/elements/header-popup.html', "Taxes ${action}ed") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
<% header(emt("Services moved")) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
% } else { #success XXX better msg talking about vacation vs. redirect all
<% include('/elements/header-popup.html', 'Rule updated') %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
<% header(emt("Package changed")) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
--- /dev/null
+<& 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;
+};
+
+</%init>
%} else {
<& /elements/header-popup.html, 'Credit successful' &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
%} else {
<% header(emt('Credit package changed')) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
%
<% header(emt('Credit successful')) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
<% header("Census tract changed") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<% header("Location changed") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
% 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" ) %>
%
% $act = 'deleted' if($attachnum and $delete);
<% header('Attachment ' . $act ) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
<% include('/elements/header-popup.html', 'Addition successful' ) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
<% include('/elements/header-popup.html', 'Addition successful' ) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
%} else {
<% header('Note ' . ($notenum ? 'updated' : 'added') ) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
<P STYLE="font-weight: bold;"><% emt($message) %></P>
<P><% emt('Please wait while the page reloads.') %></P>
<SCRIPT TYPE="text/javascript">
-window.top.location.reload();
+topreload();
</SCRIPT>
% }
%} else {
<% header(emt('Payment package changed')) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
%
<% header(emt('Payment entered')) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
<FONT SIZE="+1" COLOR="#ff0000">Error: <% $error |h %></FONT>
% } else {
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
% }
</BODY>
$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";
% } else {
<% header($action) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
<% header("Discount applied") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<& /elements/header-popup.html, "Quantity changed" &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<& /elements/header-popup.html, "Sales Person changed" &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
%
<% header('Refund entered') %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% } else {
<% header("Tax adjustment added") %>
<SCRIPT TYPE="text/javascript">
- //window.top.location.reload();
+ //topreload();
parent.cClick();
</SCRIPT>
</BODY></HTML>
<% header(emt("Package detached")) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
%} elsif ( $recnum ) { #editing
<% header('Nameservice record changed') %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
%} else { #adding
%} else {
<% header("$src_thing application$to sucessful") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<% include('/elements/header-popup.html', $opt{'popup_reload'} ) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
+% if ( $error ) {
+% $cgi->param('error', $error );
<% $cgi->redirect($redirect) %>
+% } else {
+<% header(emt($message)) %>
+ <SCRIPT TYPE="text/javascript">
+ topreload();
+ </SCRIPT>
+ </BODY></HTML>
+% }
<%init>
my $curuser = $FS::CurrentUser::CurrentUser;
% } else {
<% header($action) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
--- /dev/null
+% unless ( $opt{'js_only'} ) {
+
+ <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
+
+ <& 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"),
+ &>
+ <B><% $money_char %></B>
+ <& input-text.html,
+ field => "${name}_amount",
+ size => 8,
+ curr_value => $commission_rate->get("amount")
+ || '0.00',
+ 'text-align' => 'right'
+ &>
+ <B> + </B>
+ <& input-text.html,
+ field => "${name}_percent",
+ size => 8,
+ curr_value => $commission_rate->get("percent")
+ || '0',
+ 'text-align' => 'right'
+ &><B>%</B>
+% }
+<%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;
+}
+
+</%init>
% }
<% include('init_overlib.html') |n %>
<% include('rs_init_object.html') |n %>
-
+ <script type="text/javascript" src="<% $fsurl %>elements/topreload.js"></script>
<% $head |n %>
%# announce our base path, and the Mason comp path of this page
<SCRIPT SRC="<% $fsurl %>elements/printtofit.js"></SCRIPT>
% }
% }
+ <SCRIPT SRC="<% $fsurl %>elements/topreload.js"></SCRIPT>
<% $head |n %>
</HEAD>
<BODY <% $etc |n %>>
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',
--- /dev/null
+<%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 <SELECT> HTML
+ 'layers_only' => 0, #set true to return only the layers <DIV> HTML
+ )
+
+</%doc>
+% unless ( grep $opt{$_}, qw(html_only js_only select_only layers_only) ) {
+<SCRIPT TYPE="text/javascript">
+% }
+% unless ( grep $opt{$_}, qw(html_only select_only layers_only) ) {
+
+% unless ($selectlayersx_init) {
+
+var selectlayerx_info = {};
+
+function selectlayersx_changed (field) {
+
+ var what = document.getElementById(field);
+ selectlayerx_info[field]['onchange'](what);
+
+ var selectedlayer = what.options[what.selectedIndex].value;
+ for (i=0; i < selectlayerx_info[field]['layers'].length; i++) {
+ var iterlayer = selectlayerx_info[field]['layers'][i];
+ var iterobj = document.getElementById(field+'d'+iterlayer);
+ if (selectedlayer == iterlayer) {
+ iterobj.style.display = "";
+ iterobj.style.zIndex = 1;
+ } else {
+ iterobj.style.display = "none";
+ iterobj.style.zIndex = 0;
+ }
+ }
+
+}
+
+% $selectlayersx_init = 1;
+% } #selectlayersx_init
+
+selectlayerx_info['<% $key %>'] = {};
+selectlayerx_info['<% $key %>']['onchange'] = function (what) { <% $opt{'onchange'} %> };
+selectlayerx_info['<% $key %>']['layers'] = <% encode_json(\@layers) %>;
+
+
+% } #unless html_only/select_only/layers_only
+% unless ( grep $opt{$_}, qw(html_only js_only select_only layers_only) ) {
+</SCRIPT>
+% }
+%
+% unless ( grep $opt{$_}, qw(js_only layers_only) ) {
+
+ <SELECT NAME = "<% $key %>"
+ ID = "<% $key %>"
+ previousValue = "<% $selected %>"
+ previousText = "<% $options{$selected} %>"
+ onChange="selectlayersx_changed('<% $key %>')"
+ >
+
+% foreach my $option ( keys %$options ) {
+
+ <OPTION VALUE="<% $option %>"
+ <% $option eq $selected ? ' SELECTED' : '' %>
+ ><% $options->{$option} |h %></OPTION>
+
+% }
+
+ </SELECT>
+
+% }
+% 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;
+
+ <DIV ID="<% $key %>d<% $layer %>"
+ STYLE="<% $selected_layer eq $layer
+ ? 'display: block; z-index: 1'
+ : 'display: none; z-index: 0'
+ %>"
+ >
+
+ <% &{$layer_callback}($layer, $layer_fields, $layer_values, $layer_prefix) %>
+
+ </DIV>
+
+% }
+
+% }
+<%once>
+
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
+my $date_noinit = 0;
+
+</%once>
+<%shared>
+
+my $selectlayersx_init = 0;
+
+</%shared>
+<%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 = "<TABLE>";
+
+ 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 .= '</TABLE>';
+ return $html;
+}
+
+</%init>
--- /dev/null
+window.topreload = function() {
+ if (window != window.top) {
+ window.top.location.reload();
+ }
+}
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
--- /dev/null
+% unless ( $opt{js_only} ) {
+
+ <% include('tr-td-label.html', @_ ) %>
+
+ <TD <% $style %>>
+
+% }
+
+ <% include('selectlayersx.html', @_ ) %>
+
+% unless ( $opt{js_only} ) {
+
+ </TD>
+
+ </TR>
+
+% }
+
+<%init>
+
+my %opt = @_;
+
+my $style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+</%init>
--- /dev/null
+<& /elements/header-popup.html, mt($title) &>
+
+<& /elements/error.html &>
+
+% # only slightly different from unhold_pkg.
+<FORM NAME="MyForm" ACTION="process/change_pkg_date.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
+<INPUT TYPE="hidden" NAME="field" VALUE="<% $field %>">
+
+<BR>
+<% emt(($isstart ? 'Start billing' : 'Set contract end for').' [_1]', $part_pkg->pkg_comment(cust_pkg => $cust_pkg)) %>
+<UL STYLE="padding-left: 3ex; list-style: none; background-color: #cccccc">
+<LI>
+ <& /elements/radio.html,
+ field => 'when',
+ id => 'when_now',
+ value => 'now',
+ curr_value => $when,
+ &>
+ <label for="when_now"><% emt($isstart ? 'Now' : 'Never') %></label>
+</LI>
+% if ( $next_bill_date ) {
+<LI>
+ <& /elements/radio.html,
+ field => 'when',
+ id => 'when_next_bill_date',
+ value => 'next_bill_date',
+ curr_value => $when,
+ &>
+ <label for="when_next_bill_date">
+ <% emt('On the next bill date: [_1]',
+ time2str($date_format, $next_bill_date) ) %>
+ </label>
+</LI>
+% }
+<LI>
+<& /elements/radio.html,
+ field => 'when',
+ id => 'when_date',
+ value => 'date',
+ curr_value => $when,
+&>
+<label for="when_date"> <% emt('On this date:') %> </label>
+<& /elements/input-date-field.html,
+ { name => 'date_value',
+ value => $cgi->param('date_value') || $cust_pkg->get($field),
+ }
+&>
+</LI>
+</UL>
+<INPUT TYPE="submit" NAME="submit" VALUE="<% emt('Set '.($isstart ? 'start date' : 'contract end')) %>">
+
+</FORM>
+</BODY>
+</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);
+
+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';
+ }
+}
+</%init>
+++ /dev/null
-<& /elements/header-popup.html, mt($title) &>
-
-<& /elements/error.html &>
-
-% # only slightly different from unhold_pkg.
-<FORM NAME="MyForm" ACTION="process/change_pkg_start.html" METHOD=POST>
-<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
-
-<BR>
-<% emt('Start billing [_1]', $part_pkg->pkg_comment(cust_pkg => $cust_pkg)) %>
-<UL STYLE="padding-left: 3ex; list-style: none; background-color: #cccccc">
-<LI>
- <& /elements/radio.html,
- field => 'when',
- id => 'when_now',
- value => 'now',
- curr_value => $when,
- &>
- <label for="when_now"><% emt('Immediately') %></label>
-</LI>
-% if ( $next_bill_date ) {
-<LI>
- <& /elements/radio.html,
- field => 'when',
- id => 'when_next_bill_date',
- value => 'next_bill_date',
- curr_value => $when,
- &>
- <label for="when_next_bill_date">
- <% emt('On the next bill date: [_1]',
- time2str($date_format, $next_bill_date) ) %>
- </label>
-</LI>
-% }
-<LI>
-<& /elements/radio.html,
- field => 'when',
- id => 'when_date',
- value => 'date',
- curr_value => $when,
-&>
-<label for="when_date"> <% emt('On this date:') %> </label>
-<& /elements/input-date-field.html,
- { name => 'start_date',
- value => $cgi->param('start_date') || $cust_pkg->start_date,
- }
-&>
-</LI>
-</UL>
-<INPUT TYPE="submit" NAME="submit" VALUE="<% emt('Set start date') %>">
-
-</FORM>
-</BODY>
-</HTML>
-
-<%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';
- }
-}
-</%init>
<& /elements/header-popup.html, mt("Customer cancelled") &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<& /elements/header-popup.html, mt("Customer suspended") &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<& /elements/header-popup.html, mt("Customer unsuspended") &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
% } else {
<& /elements/header-popup.html, "Address range deleted" &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
% } else {
<% header('Rate deleted') %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
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;
<% header("Location disabled") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
% } else {
<& /elements/header-popup.html, "Template ${actioned}" &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
%} else {
<% header('Packages Adjusted') %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
% }
<% header(emt("Package $past_method")) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<% header(emt("Package contact $past_method")) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
--- /dev/null
+<& /elements/header-popup.html &>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+ </BODY>
+</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);
+}
+</%init>
+++ /dev/null
-<& /elements/header-popup.html &>
- <SCRIPT TYPE="text/javascript">
- window.top.location.reload();
- </SCRIPT>
- </BODY>
-</HTML>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-die "access denied"
- unless $curuser->access_right('Change 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);
-}
-</%init>
-<SCRIPT TYPE="text/javascript">window.top.location.reload()</SCRIPT>
+<SCRIPT TYPE="text/javascript">topreload()</SCRIPT>
<%init>
# XXX ACL?
die "access denied"
<% header($msg) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
<% include('/elements/header-popup.html', $title) %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
<& /elements/header-popup.html, 'Interface added' &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
<%init>
<& /elements/header-popup.html, 'Router added' &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
<%init>
%} else {
<% header("Package recharged") %>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
%}
<& /elements/header-popup.html &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
%} else {
<& /elements/header-popup.html, 'Invoice voided' &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY></HTML>
%}
% if ($success) {
<% include('/elements/header-popup.html', 'Reason Merge Success') %>
<SCRIPT>
-window.top.location.reload()
+topreload()
</SCRIPT>
% } else {
<% include('/elements/header-popup.html', 'Merge Reasons') %>
%if ( $success ) {
<& /elements/header-popup.html, mt("Credit voided") &>
<SCRIPT TYPE="text/javascript">
- window.top.location.reload();
+ topreload();
</SCRIPT>
</BODY>
</HTML>
#payment
'Date',
- 'Order Number',
+ @on_header,
'By',
#application
? 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 ) },
'', #payinfo/paymask
'', #cardtype
'cust_pay_date',
- '', #order_number
+ @on_null, #order_number
'', #'otaker',
'', #amount
'', #line item description
'',
'',
'',
- '',
+ @on_null,
'',
'',
'',
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(),
'',
'',
'',
- '',
+ @on_null,
'',
'',
'',
'',
'',
'',
- '',
+ @on_null,
'',
'',
'',
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;
'name_singular' => emt('payment'),
'name_verb' => emt('paid'),
'show_card_type' => 1,
- 'show_order_number' => 1,
&>
my %statusaction = (
'new' => 'delete',
'pending' => 'complete',
- #'authorized' => '',
+ 'authorized' => 'complete',
'captured' => 'capture',
#'declined' => '',
#wouldn't need to take action on a done state#'done'
'Amex' => q['American Express card'],
'Discover' => q['Discover card'],
'Maestro' => q['Switch', 'Solo', 'Laser'],
+ 'Tokenized' => q['Tokenized'],
);
</%shared>
<%init>
$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;
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'),
);
};
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, '';
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)";
}
'addl_from' => $addl_from,
};
-warn Dumper \$sql_query;
-
} else {
#hmm... is this still used?
'value' => 1,
&>
+ <& /elements/tr-checkbox.html,
+ 'label' => emt('Include order number'),
+ 'field' => 'show_order_number',
+ 'value' => 1,
+ &>
+
</TABLE>
% }
# 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,
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') {
<TD>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'),
&>
</TD>
<%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;
}
};
-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);
</%once>
unless $curuser->access_right([ 'View system logs', 'Configuration' ]);
my @menubar = ();
-push @menubar, qq(<A HREF="${fsurl}browse/log_email.html" STYLE="text-decoration: underline;">Configure conditions for sending email when logging</A>),
+push @menubar, qq(<A HREF="${fsurl}browse/log_email.html" STYLE="text-decoration: underline;">Configure conditions for sending email when logging</A>);
$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) ];
field => 'paid',
&>
+ <& /elements/tr-checkbox.html,
+ 'label' => emt('Display order number'),
+ 'field' => 'show_order_number',
+ 'value' => 1,
+ 'cell_style' => 'font-weight: normal', #for consistency
+ &>
+
<!--
<TR>
<TD ALIGN="right"><INPUT TYPE="checkbox" NAME="nottax" VALUE="Y" onClick="nottax_changed(this)" onChange="nottax_change(thid)"></TD>
--- /dev/null
+<& /elements/header-popup.html, mt($title) &>
+
+<FORM ACTION="sqlradius_usage.html" METHOD="GET" TARGET="_top">
+
+<& /elements/hidden.html,
+ 'field' => 'custnum',
+ 'value' => $custnum,
+&>
+<TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+% if ( scalar(@exports) == 1 ) {
+<tr><td>
+<& /elements/hidden.html,
+ 'field' => 'exportnum',
+ 'value' => $exports[0]->exportnum,
+&>
+</td></tr>
+% } else {
+<& /elements/tr-select-table.html,
+ 'label' => 'Export', # kind of non-indicative...
+ 'table' => 'part_export',
+ 'name_col' => 'label',
+ 'value_col' => 'exportnum',
+ 'records' => \@exports,
+ 'disable_empty' => 1,
+&>
+% }
+<& /elements/tr-input-beginning_ending.html &>
+
+</TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+</FORM>
+
+<& /elements/footer.html &>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+ unless $curuser->access_right('Usage: RADIUS sessions');
+ # yes?
+
+my $title = 'Data Usage Report';
+my $custnum;
+if ($cgi->keywords) {
+ ($custnum) = $cgi->keywords;
+} else {
+ $custnum = $cgi->param('custnum');
+}
+$custnum =~ /^(\d+)$/
+ or die "illegal custnum $custnum";
+my $cust_main = qsearchs( {
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $custnum },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+# get all exports that apply to this customer's services--should be fast, as
+# everything here is indexed
+my @exports = qsearch({
+ 'table' => 'part_export',
+ 'select' => 'DISTINCT part_export.*',
+ 'addl_from' => ' JOIN export_svc USING (exportnum)
+ JOIN cust_svc USING (svcpart)
+ JOIN cust_pkg USING (pkgnum) ',
+ 'extra_sql' => ' WHERE cust_pkg.custnum = '.$custnum,
+});
+@exports = grep { $_->can('usage_sessions') } @exports;
+
+</%init>
+%# some overlap with report_sqlradius_usage_custnum.html
<& /elements/header.html, mt($title) &>
<FORM ACTION="sqlradius_usage.html" METHOD="GET">
@svc_fields,
@svc_usage,
],
+ 'order_by_sql' => $order_by_sql,
'links' => [ #( map { $_ ne 'Cust. Status' ? $link_cust : '' }
# FS::UI::Web::cust_header() ),
$link_cust,
my %opt = @_;
-die "access denied" unless
- $FS::CurrentUser::CurrentUser->access_right('List services');
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied" unless $curuser->access_right('List services');
my $title = 'Data Usage Report - ';
my $agentnum;
$title .= time2str('%h %o %Y', $ending);
}
+# can also show a specific customer / service. the main query will handle
+# agent restrictions, but we need a list of the services to ask the export
+# for usage data.
+my ($cust_main, @svc_x);
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+ $cust_main = qsearchs( {
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $1 },
+ 'extra_sql' => ' AND '. $curuser->agentnums_sql,
+ });
+ die "Customer not found!" unless $cust_main;
+ # then only report on this agent
+ $agentnum = $cust_main->agentnum;
+ @include_agents = ();
+ # and announce that we're doing it
+ $title .= ' - ' . $cust_main->name_short;
+
+ # yes, we'll query the database once for each service the customer has,
+ # even non-radacct'd services. probably less bad than a single query that
+ # pulls records for every service for every customer.
+ foreach my $cust_pkg ($cust_main->all_pkgs) {
+ foreach my $cust_svc ($cust_pkg->cust_svc) {
+ push @svc_x, $cust_svc->svc_x;
+ }
+ }
+}
+foreach ($cgi->param('svcnum')) {
+ if (/^(\d+)$/) {
+ my $cust_svc = FS::cust_svc->by_key($1)
+ or die "service #$1 not found."; # or continue?
+ push @svc_x, $cust_svc->svc_x;
+ }
+}
+
my $export;
my %usage_by_username;
if ( exists($opt{usage_by_username}) ) {
or die "exportnum ".$export->exportnum." is type ".$export->exporttype.
", not sqlradius";
- my $usage = $export->usage_sessions( {
+ my %usage_param = (
stoptime_start => $beginning,
stoptime_end => $ending,
summarize => 1
- } );
- # arrayref of hashrefs of
+ );
+ # usage_sessions() returns an arrayref of hashrefs of
# (username, acctsessiontime, acctinputoctets, acctoutputoctets)
# (XXX needs to include 'realm' for sqlradius_withdomain)
- # rearrange to be indexed by username.
+ my $usage;
+ if ( @svc_x ) {
+ # then query once per service
+ $usage = [];
+ foreach my $svc ( @svc_x ) {
+ $usage_param{'svc'} = $svc;
+ push @$usage, @{ $export->usage_sessions(\%usage_param) };
+ }
+ } else {
+ # one query, get everyone's data
+ my $usage = $export->usage_sessions(\%usage_param);
+ }
+ # rearrange to be indexed by username.
foreach (@$usage) {
my $username = $_->{'username'};
my @row = (
my %search_hash = ( 'agentnum' => $agentnum,
'exportnum' => $export->exportnum );
+if ($cust_main) {
+ $search_hash{'custnum'} = $cust_main->custnum;
+}
+if (@svc_x) {
+ $search_hash{'svcnum'} = [ map { $_->get('svcnum') } @svc_x ];
+}
+
my $sql_query = $class->search(\%search_hash);
$sql_query->{'select'} .= ', part_pkg.pkg';
$sql_query->{'addl_from'} .= ' LEFT JOIN part_pkg USING (pkgpart)';
+if ( @svc_x ) {
+ my $svcnums = join(',', map { $_->get('svcnum') } @svc_x);
+ $sql_query->{'extra_sql'} .= ' AND svcnum IN('.$svcnums.')';
+}
+
my $link_svc = [ $p.'view/cust_svc.cgi?', 'svcnum' ];
my $link_cust = [ $p.'view/cust_main.cgi?', 'custnum' ];
# columns between the customer name and the usage fields
my $skip_cols = 1 + scalar(@svc_header);
+my $num_rows = FS::Record->scalar_sql($sql_query->{count_query});
my @footer = (
'',
- FS::Record->scalar_sql($sql_query->{count_query}) . ' services',
+ emt('[quant,_1,service]', $num_rows),
('') x $skip_cols,
map {
my $i = $_;
$_[0] ? sprintf('%.3f', $_[0] / (1024*1024*1024.0)) : '';
}
+my $conf = new FS::Conf;
+my $order_by_sql = {
+ 'name' => "CASE WHEN cust_main.company IS NOT NULL
+ AND cust_main.company != ''
+ THEN CONCAT(cust_main.company,' (',cust_main.last,', ',cust_main.first,')')
+ ELSE CONCAT(cust_main.last,', ',cust_main.first)
+ END",
+ 'display_custnum' => $conf->exists('cust_main-default_agent_custid')
+ ? "CASE WHEN cust_main.agent_custid IS NOT NULL
+ AND cust_main.agent_custid != ''
+ AND cust_main.agent_custid ". regexp_sql. " '^[0-9]+\$'
+ THEN CAST(cust_main.agent_custid AS BIGINT)
+ ELSE cust_main.custnum
+ END"
+ : "custnum",
+};
+
+#warn Dumper \%usage_by_username;
+
</%init>
url => "search/report_svc_acct.html?custnum=$custnum",
},
{
+ label => 'View data usage',
+ popup => "search/report_sqlradius_usage-custnum.html?$custnum",
+ acl => 'Usage: RADIUS sessions',
+ actionlabel => 'Data usage report',
+ width => 480,
+ height => 245,
+ },
+ {
label => 'View CDRs',
url => "search/report_cdr.html?custnum=$custnum",
+ # XXX should have a condition that the customer has any CDR packages
},
],
[
<TR>
<TD COLSPAN=<%$opt{colspan}%>>
<FONT SIZE=-1>
+% if ( !$cust_pkg->change_to_pkgnum # because on a technical level, change won't propagate,
+% # and there's not really a use case worth making that work
+% and $part_pkg->freq # technically possible to have contract_end w/o freq, but nonsensical
+% and $curuser->access_right('Change package contract end date')
+% ) {
+ ( <% pkg_change_contract_end_link($cust_pkg) %> )
+ <BR>
+% }
% if ( $cust_pkg->change_to_pkgnum ) {
% # then you can modify the package change
% if ( $curuser->access_right('Change customer package') ) {
<% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %>
<% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
-% if ( !$opt{no_links}
-% and !$change_from
-% and !$supplemental # can be changed from its main package
-% and $curuser->access_right('Change package start date') )
-% {
-
- <TR>
- <TD COLSPAN=<%$opt{colspan}%>>
- <FONT SIZE=-1>
- ( <% pkg_change_start_link($cust_pkg) %> )
- </FONT>
- </TD>
- </TR>
-% }
-
% }
%
% } else { #setup
<TR>
<TD COLSPAN=<%$opt{colspan}%>>
<FONT SIZE=-1>
+
+% #change date links
+% if ( !$change_from and !$supplemental ) {
+% my $has_date_links = 0;
+% if ( !$cust_pkg->get('setup')
+% and $curuser->access_right('Change package start date')
+% ) {
+ ( <% pkg_change_start_link($cust_pkg) %> )
+% $has_date_links = 1;
+% }
+% if ( !$cust_pkg->change_to_pkgnum # because on a technical level, change won't propagate,
+% # and there's not really a use case worth making that work
+% and $curuser->access_right('Change package contract end date')
+% ) {
+ ( <% pkg_change_contract_end_link($cust_pkg) %> )
+% $has_date_links = 1;
+% }
+% if ($has_date_links) {
+ <BR>
+% }
+% }
+
% # action links
% if ( $change_from ) {
% # nothing
sub pkg_change_start_link {
my $cust_pkg = shift;
include( '/elements/popup_link-cust_pkg.html',
- 'action' => $p . 'misc/change_pkg_start.html',
+ 'action' => $p . 'misc/change_pkg_date.html?field=start_date',
'label' => emt('Set start date'),
'actionlabel' => emt('Set start of billing for'),
'cust_pkg' => $cust_pkg,
)
}
+sub pkg_change_contract_end_link {
+ my $cust_pkg = shift;
+ include( '/elements/popup_link-cust_pkg.html',
+ 'action' => $p . 'misc/change_pkg_date.html?field=contract_end',
+ 'label' => emt('Set contract end'),
+ 'actionlabel' => emt('Set contract end for'),
+ 'cust_pkg' => $cust_pkg,
+ 'width' => 510,
+ 'height' => 310,
+ )
+}
+
sub svc_recharge_link {
include( '/elements/popup_link-cust_svc.html',
'action' => $p. 'misc/recharge_svc.html',
'new' => 'delete',
'thirdparty' => 'delete',
'pending' => 'complete',
+ 'authorized' => 'complete',
'captured' => 'capture',
);