push @where, "refnum = ".$opt{'refnum'};
}
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
$self->scalar_sql(
"SELECT COUNT(*) FROM cust_main $join WHERE ".join(' AND ', @where)
);
$self->in_time_period_and_agent($speriod, $eperiod, $agentnum),
);
+ # yuck, false laziness
push @where, "cust_main.refnum = ". $opt{'refnum'} if $opt{'refnum'};
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
my $total_sql = "SELECT COALESCE(SUM(cust_bill_pkg.setup),0)
FROM cust_bill_pkg
$cust_bill_pkg_join
push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
# subtract all usage from the line item regardless of date
my $item_usage;
if ( $opt{'project'} ) {
push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
$agentnum ||= $opt{'agentnum'};
push @where,
if ( $opt{'refnum'} =~ /^(\d+)$/ ) {
$sql .= " and refnum = $1 ";
}
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ $sql .= ' and cust_main.classnum in('. join(',',@$classnums) .')'
+ if @$classnums;
+ }
+
$sql;
}
#opt
'agentnum' => 54
'refnum' => 54
+ 'cust_classnum' => [ 1,2,4 ],
'params' => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
'remove_empty' => 1, #collapse empty rows, default 0
'item_labels' => [ ], #useful with remove_empty
=item refnum: Limit to customers with this advertising source.
+=item cust_classnum: Limit to customers with this classnum; can be an
+arrayref.
+
=item remove_empty: Set this to a true value to hide rows that contain
only zeroes. The C<indices> array in the returned data will list the item
indices that are actually present in the output so that you know what they
my $agentnum = $self->{'agentnum'};
my $refnum = $self->{'refnum'};
+ my $cust_classnum = $self->{'cust_classnum'} || [];
+ $cust_classnum = [ $cust_classnum ] if !ref($cust_classnum);
if ( $projecting ) {
my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
push @param, 'project', $projecting;
push @param, 'refnum' => $refnum if $refnum;
+ push @param, 'cust_classnum' => $cust_classnum if @$cust_classnum;
if ( $self->{'cross_params'} ) {
my @xdata;
'depositor', 'varchar', 'NULL', $char_d, '', '',
'account', 'varchar', 'NULL', 20, '', '',
'teller', 'varchar', 'NULL', 20, '', '',
+
'batchnum', 'int', 'NULL', '', '', '', #pay_batch foreign key
+
+ # credit card/EFT fields (formerly in paybatch)
+ 'gatewaynum', 'int', 'NULL', '', '', '', # payment_gateway FK
+ 'processor', 'varchar', 'NULL', $char_d, '', '', # module name
+ 'auth', 'varchar','NULL',16, '', '', # CC auth number
+ 'order_number','varchar','NULL',$char_d, '', '', # transaction number
],
'primary_key' => 'paynum',
#i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it# 'unique' => [ [ 'payunique' ] ],
'teller', 'varchar', 'NULL', 20, '', '',
'batchnum', 'int', 'NULL', '', '', '', #pay_batch foreign key
+ # credit card/EFT fields (formerly in paybatch)
+ 'gatewaynum', 'int', 'NULL', '', '', '', # payment_gateway FK
+ 'processor', 'varchar', 'NULL', $char_d, '', '', # module name
+ 'auth', 'varchar','NULL',16, '', '', # CC auth number
+ 'order_number', 'varchar','NULL',$char_d, '', '', # transaction number
+
#void fields
'void_date', @date_type, '', '',
'reason', 'varchar', 'NULL', $char_d, '', '',
'paymask', 'varchar', 'NULL', $char_d, '', '',
'paybatch', 'varchar', 'NULL', $char_d, '', '',
'closed', 'char', 'NULL', 1, '', '',
+ # credit card/EFT fields (formerly in paybatch)
+ 'gatewaynum', 'int', 'NULL', '', '', '', # payment_gateway FK
+ 'processor', 'varchar', 'NULL', $char_d, '', '', # module name
+ 'auth', 'varchar','NULL',16, '', '', # CC auth number
+ 'order_number', 'varchar','NULL',$char_d, '', '', # transaction number
],
'primary_key' => 'refundnum',
'unique' => [],
# bypass RT ACLs--we're going to do lots of things
my $CurrentUser = $RT::SystemUser;
+ my $dbh = dbh;
+
# selfservice and cron users
foreach my $username ('%%%SELFSERVICE_USER%%%', 'fs_daily') {
my $User = RT::User->new($CurrentUser);
die $msg if !$val;
} #foreach (@Scrips)
+ # one-time fix: accumulator fields (support time, etc.) that had values
+ # entered on ticket creation need OCFV records attached to their Create
+ # transactions
+ my $sql = 'SELECT first_ocfv.ObjectId, first_ocfv.Created, Content '.
+ 'FROM ObjectCustomFieldValues as first_ocfv '.
+ 'JOIN ('.
+ # subquery to get the first OCFV with a certain name for each ticket
+ 'SELECT min(ObjectCustomFieldValues.Id) AS Id '.
+ 'FROM ObjectCustomFieldValues '.
+ 'JOIN CustomFields '.
+ 'ON (ObjectCustomFieldValues.CustomField = CustomFields.Id) '.
+ 'WHERE ObjectType = \'RT::Ticket\' '.
+ 'AND CustomFields.Name = ? '.
+ 'GROUP BY ObjectId'.
+ ') AS first_ocfv_id USING (Id) '.
+ 'JOIN ('.
+ # subquery to get the first transaction date for each ticket
+ # other than the Create
+ 'SELECT ObjectId, min(Created) AS Created FROM Transactions '.
+ 'WHERE ObjectType = \'RT::Ticket\' '.
+ 'AND Type != \'Create\' '.
+ 'GROUP BY ObjectId'.
+ ') AS first_txn ON (first_ocfv.ObjectId = first_txn.ObjectId) '.
+ # where the ticket custom field acquired a value before any transactions
+ # on the ticket (i.e. it was set on ticket creation)
+ 'WHERE first_ocfv.Created < first_txn.Created '.
+ # and we haven't already fixed the ticket
+ 'AND NOT EXISTS('.
+ 'SELECT 1 FROM Transactions JOIN ObjectCustomFieldValues '.
+ 'ON (Transactions.Id = ObjectCustomFieldValues.ObjectId) '.
+ 'JOIN CustomFields '.
+ 'ON (ObjectCustomFieldValues.CustomField = CustomFields.Id) '.
+ 'WHERE ObjectCustomFieldValues.ObjectType = \'RT::Transaction\' '.
+ 'AND CustomFields.Name = ? '.
+ 'AND Transactions.Type = \'Create\''.
+ 'AND Transactions.ObjectType = \'RT::Ticket\''.
+ 'AND Transactions.ObjectId = first_ocfv.ObjectId'.
+ ')';
+ #whew
+
+ # prior to this fix, the only name an accumulate field could possibly have
+ # was "Support time".
+ my $sth = $dbh->prepare($sql);
+ $sth->execute('Support time', 'Support time');
+ my $rows = $sth->rows;
+ warn "Fixing support time on $rows rows...\n" if $rows > 0;
+ while ( my $row = $sth->fetchrow_arrayref ) {
+ my ($tid, $created, $content) = @$row;
+ my $Txns = RT::Transactions->new($CurrentUser);
+ $Txns->Limit(FIELD => 'ObjectId', VALUE => $tid);
+ $Txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
+ $Txns->Limit(FIELD => 'Type', VALUE => 'Create');
+ my $CreateTxn = $Txns->First;
+ if ($CreateTxn) {
+ my ($val, $msg) = $CreateTxn->AddCustomFieldValue(
+ Field => 'Support time',
+ Value => $content,
+ RecordTransaction => 0,
+ );
+ warn "Error setting transaction support time: $msg\n" unless $val;
+ } else {
+ warn "Create transaction not found for ticket $tid.\n";
+ }
+ }
+
return;
}
# we failed to successfully create the user. abort abort abort.
delete $session->{'CurrentUser'};
- die "can't auto-create RT user"; #an error message would be nice :/
+ die "can't auto-create RT user: $msg"; #an error message would be nice :/
#$m->abort() unless $RT::WebFallbackToInternalAuth;
#$m->comp( '/Elements/Login', %ARGS,
# Error => loc( 'Cannot create user: [_1]', $msg ) );
push @search, "cust_bill.custnum = $1";
}
+ #customer classnum
+ if ( $param->{'cust_classnum'} ) {
+ my $classnums = $param->{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ $classnums = [ grep /^\d+$/, @$classnums ];
+ push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
+ if @$classnums;
+ }
+
#_date
if ( $param->{_date} ) {
my($beginning, $ending) = @{$param->{_date}};
my $tax_location = $self->get('cust_bill_pkg_tax_location');
if ( $tax_location ) {
- foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
- $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
- $error = $cust_bill_pkg_tax_location->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "error inserting cust_bill_pkg_tax_location: $error";
+ foreach my $link ( @$tax_location ) {
+ next if $link->billpkgtaxlocationnum; # don't try to double-insert
+ # This cust_bill_pkg can be linked on either side (i.e. it can be the
+ # tax or the taxed item). If the other side is already inserted,
+ # then set billpkgnum to ours, and insert the link. Otherwise,
+ # set billpkgnum to ours and pass the link off to the cust_bill_pkg
+ # on the other side, to be inserted later.
+
+ my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
+ if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
+ $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
+ # break circular links when doing this
+ $link->set('tax_cust_bill_pkg', '');
}
- }
+ my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
+ if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
+ $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
+ # XXX if we ever do tax-on-tax for these, this will have to change
+ # since pkgnum will be zero
+ $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
+ $link->set('locationnum',
+ $taxable_cust_bill_pkg->cust_pkg->tax_locationnum);
+ $link->set('taxable_cust_bill_pkg', '');
+ }
+
+ if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
+ $error = $link->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_bill_pkg_tax_location: $error";
+ }
+ } else { # handoff
+ my $other;
+ $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
+ : $link->get('tax_cust_bill_pkg');
+ my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
+ push @$link_array, $link;
+ $other->set('cust_bill_pkg_tax_location' => $link_array);
+ }
+ } #foreach my $link
}
+ # someday you will be as awesome as cust_bill_pkg_tax_location...
+ # but not today
my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
if ( $tax_rate_location ) {
foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
'setuptax' => '', # or 'Y' for tax exempt
+ 'locationnum'=> 1234, # optional
+
#internal taxation
'taxclass' => 'Tax class',
my $no_auto = '';
my $cust_pkg_ref = '';
my ( $bill_now, $invoice_terms ) = ( 0, '' );
+ my $locationnum;
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
$cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
$bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
$invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
+ $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
} else {
$amount = shift;
$quantity = 1;
'quantity' => $quantity,
'start_date' => $start_date,
'no_auto' => $no_auto,
+ 'locationnum'=> $locationnum,
} );
$error = $cust_pkg->insert;
=item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
-This is a weird one. Perhaps it should not even be exposed.
-
Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
Usually used internally by bill method B<bill>.
# values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
my %tax_rate_location = ();
- # keys are taxnums (not internal identifiers!)
+ # keys are taxlisthash keys (internal identifiers!)
# values are arrayrefs of cust_tax_exempt_pkg objects
my %tax_exemption;
# It also calculates exemptions and attaches them to the cust_bill_pkgs
# in the argument.
my $taxables = $taxlisthash->{$tax};
- my $exemptions = $tax_exemption{$tax_object->taxnum} ||= [];
- my $hashref_or_error =
- $tax_object->taxline( $taxables,
+ my $exemptions = $tax_exemption{$tax} ||= [];
+ my $taxline = $tax_object->taxline(
+ $taxables,
'custnum' => $self->custnum,
'invoice_time' => $invoice_time,
'exemptions' => $exemptions,
);
- return $hashref_or_error unless ref($hashref_or_error);
-
- # then collect any new exemptions generated for this tax
- push @$exemptions, @{ $_->cust_tax_exempt_pkg }
- foreach @$taxables;
+ return $taxline unless ref($taxline);
unshift @{ $taxlisthash->{$tax} }, $tax_object;
- my $name = $hashref_or_error->{'name'};
- my $amount = $hashref_or_error->{'amount'};
+ if ( $tax_object->isa('FS::cust_main_county') ) {
+ # then $taxline is a real line item
+ push @{ $taxname{ $taxline->itemdesc } }, $taxline;
- #warn "adding $amount as $name\n";
- $taxname{ $name } ||= [];
- push @{ $taxname{ $name } }, $tax;
+ } else {
+ # leave this as is for now
- $tax_amount{ $tax } += $amount;
+ my $name = $taxline->{'name'};
+ my $amount = $taxline->{'amount'};
- # link records between cust_main_county/tax_rate and cust_location
- $tax_location{ $tax } ||= [];
- $tax_rate_location{ $tax } ||= [];
- if ( ref($tax_object) eq 'FS::cust_main_county' ) {
- push @{ $tax_location{ $tax } },
- {
- 'taxnum' => $tax_object->taxnum,
- 'taxtype' => ref($tax_object),
- 'pkgnum' => $tax_object->get('pkgnum'),
- 'locationnum' => $tax_object->get('locationnum'),
- 'amount' => sprintf('%.2f', $amount ),
- 'taxable_billpkgnum' => $tax_object->get('billpkgnum'),
- };
- }
- elsif ( ref($tax_object) eq 'FS::tax_rate' ) {
+ #warn "adding $amount as $name\n";
+ $taxname{ $name } ||= [];
+ push @{ $taxname{ $name } }, $tax;
+
+ $tax_amount{ $tax } += $amount;
+
+ # link records between cust_main_county/tax_rate and cust_location
+ $tax_rate_location{ $tax } ||= [];
my $taxratelocationnum =
$tax_object->tax_rate_location->taxratelocationnum;
push @{ $tax_rate_location{ $tax } },
'amount' => sprintf('%.2f', $amount ),
'locationtaxid' => $tax_object->location,
'taxratelocationnum' => $taxratelocationnum,
- 'taxable_billpkgnum' => $tax_object->get('billpkgnum'),
};
- }
-
- }
-
- #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
- my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
- foreach my $tax ( keys %$taxlisthash ) {
- my $taxables = $taxlisthash->{$tax};
- my $tax_object = shift @$taxables; # the rest are line items
- foreach my $cust_bill_pkg ( @$taxables ) {
- next unless ref($cust_bill_pkg) eq 'FS::cust_bill_pkg'; #IS needed for CCH tax-on-tax
-
- my @cust_tax_exempt_pkg = splice @{ $cust_bill_pkg->cust_tax_exempt_pkg };
-
- next unless @cust_tax_exempt_pkg;
- # get the non-disintegrated version
- my $real_cust_bill_pkg = $packagemap{$cust_bill_pkg->pkgnum}
- or die "can't distribute tax exemptions: no line item for ".
- Dumper($_). " in packagemap ".
- join(',', sort {$a<=>$b} keys %packagemap). "\n";
-
- push @{ $real_cust_bill_pkg->cust_tax_exempt_pkg },
- @cust_tax_exempt_pkg;
- }
- }
+ } #if ref($tax_object)...
+ } #foreach keys %$taxlisthash
#consolidate and create tax line items
warn "consolidating and generating...\n" if $DEBUG > 2;
foreach my $taxname ( keys %taxname ) {
+ my @cust_bill_pkg_tax_location;
+ my @cust_bill_pkg_tax_rate_location;
+ my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
+ 'pkgnum' => 0,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $taxname,
+ 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
+ });
+
my $tax_total = 0;
my %seen = ();
- my @cust_bill_pkg_tax_location = ();
- my @cust_bill_pkg_tax_rate_location = ();
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
- next if $seen{$taxitem}++;
- warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
- $tax_total += $tax_amount{$taxitem};
- push @cust_bill_pkg_tax_location,
- map { new FS::cust_bill_pkg_tax_location $_ }
- @{ $tax_location{ $taxitem } };
- push @cust_bill_pkg_tax_rate_location,
- map { new FS::cust_bill_pkg_tax_rate_location $_ }
- @{ $tax_rate_location{ $taxitem } };
+ if ( ref($taxitem) eq 'FS::cust_bill_pkg' ) {
+ # then we need to transfer the amount and the links from the
+ # line item to the new one we're creating.
+ $tax_total += $taxitem->setup;
+ foreach my $link ( @{ $taxitem->get('cust_bill_pkg_tax_location') } ) {
+ $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
+ push @cust_bill_pkg_tax_location, $link;
+ }
+ } else {
+ # the tax_rate way
+ next if $seen{$taxitem}++;
+ warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
+ $tax_total += $tax_amount{$taxitem};
+ push @cust_bill_pkg_tax_rate_location,
+ map { new FS::cust_bill_pkg_tax_rate_location $_ }
+ @{ $tax_rate_location{ $taxitem } };
+ }
}
next unless $tax_total;
+ # we should really neverround this up...I guess it's okay if taxline
+ # already returns amounts with 2 decimal places
$tax_total = sprintf('%.2f', $tax_total );
+ $tax_cust_bill_pkg->set('setup', $tax_total);
my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
'disabled' => '',
push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
}
+ $tax_cust_bill_pkg->set('display', \@display);
- push @tax_line_items, new FS::cust_bill_pkg {
- 'pkgnum' => 0,
- 'setup' => $tax_total,
- 'recur' => 0,
- 'sdate' => '',
- 'edate' => '',
- 'itemdesc' => $taxname,
- 'display' => \@display,
- 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
- 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
- };
-
+ push @tax_line_items, $tax_cust_bill_pkg;
}
\@tax_line_items;
# handle taxes
###
- unless ( $discount_show_always ) {
- my $error =
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
- return $error if $error;
- }
+ #unless ( $discount_show_always ) { # oh, for god's sake
+ my $error = $self->_handle_taxes(
+ $part_pkg,
+ $taxlisthash,
+ $cust_bill_pkg,
+ $cust_pkg,
+ $options{invoice_time},
+ $real_pkgpart,
+ \%options # I have serious objections to this
+ );
+ return $error if $error;
+ #}
+
+ $cust_bill_pkg->set_display(
+ part_pkg => $part_pkg,
+ real_pkgpart => $real_pkgpart,
+ );
push @$cust_bill_pkgs, $cust_bill_pkg;
}
+# This is _handle_taxes. It's called once for each cust_bill_pkg generated
+# from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
+# non-overridden pkgpart, a flag indicating whether the package is being
+# canceled, and a partridge in a pear tree.
+#
+# The most important argument is 'taxlisthash'. This is shared across the
+# entire invoice. It looks like this:
+# {
+# 'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
+# 'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
+# }
+#
+# 'cust_main_county' can also be 'tax_rate'. The first object in the array
+# is always the cust_main_county or tax_rate identified by the key.
+#
+# That "..." is a list of FS::cust_bill_pkg objects that will be fed to
+# the 'taxline' method to calculate the amount of the tax. This doesn't
+# happen until calculate_taxes, though.
+
sub _handle_taxes {
my $self = shift;
my $part_pkg = shift;
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
- my %cust_bill_pkg = ();
- my %taxes = ();
-
- my @classes;
- #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
- push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
- push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
- push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
-
- my $exempt = $conf->exists('cust_class-tax_exempt')
- ? ( $self->cust_class ? $self->cust_class->tax : '' )
- : $self->tax;
- # standardize this just to be sure
- $exempt = ($exempt eq 'Y') ? 'Y' : '';
-
- #if ( $exempt !~ /Y/i && $self->payby ne 'COMP' ) {
- if ( $self->payby ne 'COMP' ) {
-
- if ( $conf->exists('enable_taxproducts')
- && ( scalar($part_pkg->part_pkg_taxoverride)
- || $part_pkg->has_taxproduct
- )
- )
- {
+ return if ( $self->payby eq 'COMP' ); #dubious
- if ( !$exempt ) {
+ if ( $conf->exists('enable_taxproducts')
+ && ( scalar($part_pkg->part_pkg_taxoverride)
+ || $part_pkg->has_taxproduct
+ )
+ )
+ {
- foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{$class} = $err_or_ref;
- }
+ # EXTERNAL TAX RATES (via tax_rate)
+ my %cust_bill_pkg = ();
+ my %taxes = ();
+
+ my @classes;
+ #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
+ push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
+ # debatable
+ push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
+ push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
+
+ my $exempt = $conf->exists('cust_class-tax_exempt')
+ ? ( $self->cust_class ? $self->cust_class->tax : '' )
+ : $self->tax;
+ # standardize this just to be sure
+ $exempt = ($exempt eq 'Y') ? 'Y' : '';
+
+ if ( !$exempt ) {
- unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{''} = $err_or_ref;
- }
+ foreach my $class (@classes) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{$class} = $err_or_ref;
+ }
+ unless (exists $taxes{''}) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{''} = $err_or_ref;
}
- } else { # cust_main_county tax system
+ }
- # We fetch taxes even if the customer is completely exempt,
- # because we need to record that fact.
+ my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
+ foreach my $key (keys %tax_cust_bill_pkg) {
+ # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
+ # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
+ # the line item.
+ # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
+ # apply to $key-class charges.
+ my @taxes = @{ $taxes{$key} || [] };
+ my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
+
+ my %localtaxlisthash = ();
+ foreach my $tax ( @taxes ) {
+
+ # this is the tax identifier, not the taxname
+ my $taxname = ref( $tax ). ' '. $tax->taxnum;
+ $taxname .= ' billpkgnum'. $cust_bill_pkg->billpkgnum;
+ # We need to create a separate $taxlisthash entry for each billpkgnum
+ # on the invoice, so that cust_bill_pkg_tax_location records will
+ # be linked correctly.
+
+ # $taxlisthash: keys are "setup", "recur", and usage classes.
+ # Values are arrayrefs, first the tax object (cust_main_county
+ # or tax_rate) and then any cust_bill_pkg objects that the
+ # tax applies to.
+ $taxlisthash->{ $taxname } ||= [ $tax ];
+ push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
+
+ $localtaxlisthash{ $taxname } ||= [ $tax ];
+ push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
- my @loc_keys = qw( district city county state country );
- my $location = $cust_pkg->tax_location;
- my %taxhash = map { $_ => $location->$_ } @loc_keys;
+ }
- $taxhash{'taxclass'} = $part_pkg->taxclass;
+ warn "finding taxed taxes...\n" if $DEBUG > 2;
+ foreach my $tax ( keys %localtaxlisthash ) {
+ my $tax_object = shift @{ $localtaxlisthash{$tax} };
+ warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
+ if $DEBUG > 2;
+ next unless $tax_object->can('tax_on_tax');
+
+ foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
+ my $totname = ref( $tot ). ' '. $tot->taxnum;
+
+ warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
+ if $DEBUG > 2;
+ next unless exists( $localtaxlisthash{ $totname } ); # only increase
+ # existing taxes
+ warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
+ # we're calling taxline() right here? wtf?
+ my $hashref_or_error =
+ $tax_object->taxline( $localtaxlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time,
+ );
+ return $hashref_or_error
+ unless ref($hashref_or_error);
+
+ $taxlisthash->{ $totname } ||= [ $tot ];
+ push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
- warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
+ }
+ }
+ }
- my @taxes = (); # entries are cust_main_county objects
- my %taxhash_elim = %taxhash;
- my @elim = qw( district city county state );
- do {
+ } else {
- #first try a match with taxclass
- @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ # INTERNAL TAX RATES (cust_main_county)
- if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
- #then try a match without taxclass
- my %no_taxclass = %taxhash_elim;
- $no_taxclass{ 'taxclass' } = '';
- @taxes = qsearch( 'cust_main_county', \%no_taxclass );
- }
+ # We fetch taxes even if the customer is completely exempt,
+ # because we need to record that fact.
- $taxhash_elim{ shift(@elim) } = '';
+ my @loc_keys = qw( district city county state country );
+ my $location = $cust_pkg->tax_location;
+ my %taxhash = map { $_ => $location->$_ } @loc_keys;
- } while ( !scalar(@taxes) && scalar(@elim) );
+ $taxhash{'taxclass'} = $part_pkg->taxclass;
- foreach (@taxes) {
- # These could become cust_bill_pkg_tax_location records,
- # or cust_tax_exempt_pkg. We'll decide later.
- #
- # The most important thing here: record which charge is being taxed.
- $_->set('billpkgnum', $cust_bill_pkg->billpkgnum);
- # also these, for historical reasons
- $_->set('pkgnum', $cust_pkg->pkgnum);
- $_->set('locationnum', $cust_pkg->tax_locationnum);
- }
+ warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
- $taxes{''} = [ @taxes ];
- $taxes{'setup'} = [ @taxes ];
- $taxes{'recur'} = [ @taxes ];
- $taxes{$_} = [ @taxes ] foreach (@classes);
-
- # # maybe eliminate this entirely, along with all the 0% records
- # unless ( @taxes ) {
- # return
- # "fatal: can't find tax rate for state/county/country/taxclass ".
- # join('/', map $taxhash{$_}, qw(state county country taxclass) );
- # }
-
- } #if $conf->exists('enable_taxproducts') ...
-
- } # if $self->payby eq 'COMP'
-
- #what's this doing in the middle of _handle_taxes? probably should split
- #this into three parts above in _make_lines
- $cust_bill_pkg->set_display( part_pkg => $part_pkg,
- real_pkgpart => $real_pkgpart,
- );
-
- my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
- foreach my $key (keys %tax_cust_bill_pkg) {
- # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
- # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
- # the line item.
- # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
- # apply to $key-class charges.
- my @taxes = @{ $taxes{$key} || [] };
- my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
-
- my %localtaxlisthash = ();
- foreach my $tax ( @taxes ) {
-
- # this is the tax identifier, not the taxname
- my $taxname = ref( $tax ). ' '. $tax->taxnum;
- $taxname .= ' billpkgnum'. $cust_bill_pkg->billpkgnum;
- # We need to create a separate $taxlisthash entry for each billpkgnum
- # on the invoice, so that cust_bill_pkg_tax_location records will
- # be linked correctly.
-
- # $taxlisthash: keys are "setup", "recur", and usage classes.
- # Values are arrayrefs, first the tax object (cust_main_county
- # or tax_rate) and then any cust_bill_pkg objects that the
- # tax applies to.
- $taxlisthash->{ $taxname } ||= [ $tax ];
- push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
-
- $localtaxlisthash{ $taxname } ||= [ $tax ];
- push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
+ my @taxes = (); # entries are cust_main_county objects
+ my %taxhash_elim = %taxhash;
+ my @elim = qw( district city county state );
+ do {
- }
+ #first try a match with taxclass
+ @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
- warn "finding taxed taxes...\n" if $DEBUG > 2;
- foreach my $tax ( keys %localtaxlisthash ) {
- my $tax_object = shift @{ $localtaxlisthash{$tax} };
- warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
- if $DEBUG > 2;
- next unless $tax_object->can('tax_on_tax');
+ if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
+ #then try a match without taxclass
+ my %no_taxclass = %taxhash_elim;
+ $no_taxclass{ 'taxclass' } = '';
+ @taxes = qsearch( 'cust_main_county', \%no_taxclass );
+ }
- foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
- my $totname = ref( $tot ). ' '. $tot->taxnum;
+ $taxhash_elim{ shift(@elim) } = '';
- warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
- if $DEBUG > 2;
- next unless exists( $localtaxlisthash{ $totname } ); # only increase
- # existing taxes
- warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
- my $hashref_or_error =
- $tax_object->taxline( $localtaxlisthash{$tax},
- 'custnum' => $self->custnum,
- 'invoice_time' => $invoice_time,
- );
- return $hashref_or_error
- unless ref($hashref_or_error);
-
- $taxlisthash->{ $totname } ||= [ $tot ];
- push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
+ } while ( !scalar(@taxes) && scalar(@elim) );
- }
+ foreach (@taxes) {
+ my $tax_id = 'cust_main_county '.$_->taxnum;
+ $taxlisthash->{$tax_id} ||= [ $_ ];
+ push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
}
}
-
'';
}
return "Error: No error; test failure requested with fake_failure";
}
- #my $paybatch = '';
- #if ( $payment_gateway->gatewaynum ) { # agent override
- # $paybatch = $payment_gateway->gatewaynum. '-';
- #}
- #
- #$paybatch .= "$processor:". $transaction->authorization;
- #
- #$paybatch .= ':'. $transaction->order_number
- # if $transaction->can('order_number')
- # && length($transaction->order_number);
-
- my $paybatch = 'FakeProcessor:54:32';
-
my $cust_pay = new FS::cust_pay ( {
'custnum' => $self->custnum,
'invnum' => $options{'invnum'},
'payby' => $bop_method2payby{$options{method}},
#'payinfo' => $payinfo,
'payinfo' => '4111111111111111',
- 'paybatch' => $paybatch,
#'paydate' => $paydate,
'paydate' => '2012-05-01',
+ 'processor' => 'FakeProcessor',
+ 'auth' => '54',
+ 'order_number' => '32',
} );
$cust_pay->payunique( $options{payunique} ) if length($options{payunique});
if ( $transaction->is_success() ) {
- my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
- $paybatch = $payment_gateway->gatewaynum. '-';
- }
-
- $paybatch .= $payment_gateway->gateway_module. ":".
- $transaction->authorization;
-
- $paybatch .= ':'. $transaction->order_number
- if $transaction->can('order_number')
- && length($transaction->order_number);
+ my $order_number = $transaction->order_number
+ if $transaction->can('order_number');
my $cust_pay = new FS::cust_pay ( {
'custnum' => $self->custnum,
'_date' => '',
'payby' => $cust_pay_pending->payby,
'payinfo' => $options{'payinfo'},
- 'paybatch' => $paybatch,
'paydate' => $cust_pay_pending->paydate,
'pkgnum' => $cust_pay_pending->pkgnum,
- 'discount_term' => $options{'discount_term'},
+ 'discount_term' => $options{'discount_term'},
+ 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
+ 'processor' => $payment_gateway->gateway_module,
+ 'auth' => $transaction->authorization,
+ 'order_number' => $order_number || '',
+
} );
#doesn't hurt to know, even though the dup check is in cust_pay_pending now
$cust_pay->payunique( $options{payunique} )
my( $processor, $login, $password, @bop_options, $namespace ) ;
my( $auth, $order_number ) = ( '', '', '' );
+ my $gatewaynum = '';
if ( $options{'paynum'} ) {
or return "Unknown paynum $options{'paynum'}";
$amount ||= $cust_pay->paid;
- $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
- or return "Can't parse paybatch for paynum $options{'paynum'}: ".
- $cust_pay->paybatch;
- my $gatewaynum = '';
- ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
+ if ( $cust_pay->get('processor') ) {
+ ($gatewaynum, $processor, $auth, $order_number) =
+ (
+ $cust_pay->gatewaynum,
+ $cust_pay->processor,
+ $cust_pay->auth,
+ $cust_pay->order_number,
+ );
+ } else {
+ # this payment wasn't upgraded, which probably means this won't work,
+ # but try it anyway
+ $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
+ or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+ $cust_pay->paybatch;
+ ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
+ }
if ( $gatewaynum ) { #gateway for the payment to be refunded
return "$processor error: ". $refund->error_message
unless $refund->is_success();
- my $paybatch = "$processor:". $refund->authorization;
- $paybatch .= ':'. $refund->order_number
- if $refund->can('order_number') && $refund->order_number;
+ $order_number = $refund->order_number if $refund->can('order_number');
while ( $cust_pay && $cust_pay->unapplied < $amount ) {
my @cust_bill_pay = $cust_pay->cust_bill_pay;
'_date' => '',
'payby' => $bop_method2payby{$options{method}},
'payinfo' => $payinfo,
- 'paybatch' => $paybatch,
'reason' => $options{'reason'} || 'card or ACH refund',
+ 'gatewaynum' => $gatewaynum, # may be null
+ 'processor' => $processor,
+ 'auth' => $refund->authorization,
+ 'order_number' => $order_number,
} );
my $error = $cust_refund->insert;
if ( $error ) {
=item taxline TAXABLES_ARRAYREF, [ OPTION => VALUE ... ]
-Returns an hashref of a name and an amount of tax calculated for the
-line items (L<FS::cust_bill_pkg> objects) in TAXABLES_ARRAYREF. The line
-items must come from the same invoice. Returns a scalar error message
-on error.
+Takes an arrayref of L<FS::cust_bill_pkg> objects representing taxable
+line items, and returns a new L<FS::cust_bill_pkg> object representing
+the tax on them under this tax rate.
+
+This will have a pseudo-field, "cust_bill_pkg_tax_location", containing
+an arrayref of L<FS::cust_bill_pkg_tax_location> objects. Each of these
+will in turn have a "taxable_cust_bill_pkg" pseudo-field linking it to one
+of the taxable items. All of these links must be resolved as the objects
+are inserted.
In addition to calculating the tax for the line items, this will calculate
any appropriate tax exemptions and attach them to the line items.
=cut
-# XXX this should just return a cust_bill_pkg object for the tax,
-# but that requires changing stuff in tax_rate.pm also.
+# XXX change tax_rate.pm to work like this
sub taxline {
my( $self, $taxables, %opt ) = @_;
my $dbh = dbh;
my $name = $self->taxname || 'Tax';
- my $amount = 0;
+ my $taxable_cents = 0;
+ my $tax_cents = 0;
my $cust_bill = $taxables->[0]->cust_bill;
my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'};
push @existing_exemptions, @{ $_->cust_tax_exempt_pkg }
for @$taxables;
+ my $tax_item = FS::cust_bill_pkg->new({
+ 'pkgnum' => 0,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $name,
+ });
+ my @tax_location;
+
foreach my $cust_bill_pkg (@$taxables) {
my $cust_pkg = $cust_bill_pkg->cust_pkg;
$_->taxnum($self->taxnum) foreach @new_exemptions;
- #if ( $cust_bill_pkg->billpkgnum ) {
-
- #no, need to do this to e.g. calculate tax credit amounts
- #die "tried to calculate tax exemptions on a previously billed line item\n";
-
- # this is unnecessary
-# foreach my $cust_tax_exempt_pkg (@new_exemptions) {
-# my $error = $cust_tax_exempt_pkg->insert;
-# if ( $error ) {
-# $dbh->rollback if $oldAutoCommit;
-# return "can't insert cust_tax_exempt_pkg: $error";
-# }
-# }
- #}
-
# attach them to the line item
push @{ $cust_bill_pkg->cust_tax_exempt_pkg }, @new_exemptions;
push @existing_exemptions, @new_exemptions;
- # If we were smart, we'd also generate a cust_bill_pkg_tax_location
- # record at this point, but that would require redesigning more stuff.
$taxable_charged = sprintf( "%.2f", $taxable_charged);
-
- $amount += $taxable_charged * $self->tax / 100;
+ next if $taxable_charged == 0;
+
+ my $this_tax_cents = int($taxable_charged * $self->tax);
+ my $location = FS::cust_bill_pkg_tax_location->new({
+ 'taxnum' => $self->taxnum,
+ 'taxtype' => ref($self),
+ 'cents' => $this_tax_cents,
+ 'taxable_cust_bill_pkg' => $cust_bill_pkg,
+ 'tax_cust_bill_pkg' => $tax_item,
+ });
+ push @tax_location, $location;
+
+ $taxable_cents += $taxable_charged;
+ $tax_cents += $this_tax_cents;
} #foreach $cust_bill_pkg
-
- return {
- 'name' => $name,
- 'amount' => $amount,
- };
-
+
+ # now round and distribute
+ my $extra_cents = sprintf('%.2f', $taxable_cents * $self->tax / 100) * 100
+ - $tax_cents;
+ if ( $extra_cents < 0 ) {
+ die "nonsense extra_cents value $extra_cents"; # because seriously, wtf
+ }
+ $tax_cents += $extra_cents;
+ my $i = 0;
+ foreach (@tax_location) { # can never require more than a single pass, yes?
+ my $cents = $_->get('cents');
+ if ( $extra_cents > 0 ) {
+ $cents++;
+ $extra_cents--;
+ }
+ $_->set('amount', sprintf('%.2f', $cents/100));
+ }
+ $tax_item->set('setup' => sprintf('%.2f', $tax_cents / 100));
+ $tax_item->set('cust_bill_pkg_tax_location', \@tax_location);
+
+ return $tax_item;
}
=back
=item paybatch
-text field for tracking card processing or other batch grouping
+obsolete text field for tracking card processing or other batch grouping
=item payunique
The teller number.
-=item pay_batch
+=item batchnum
The number of the batch this payment came from (see L<FS::pay_batch>),
or null if it was processed through a realtime gateway or entered manually.
+=item gatewaynum
+
+The number of the realtime or batch gateway L<FS::payment_gateway>) this
+payment was processed through. Null if it was entered manually or processed
+by the "system default" gateway, which doesn't have a number.
+
+=item processor
+
+The name of the processor module (Business::OnlinePayment, ::BatchPayment,
+or ::OnlineThirdPartyPayment subclass) used for this payment. Slightly
+redundant with C<gatewaynum>.
+
+=item auth
+
+The authorization number returned by the credit card network.
+
+=item order_number
+
+The transaction ID returned by the gateway, if any. This is usually what
+you would use to initiate a void or refund of the payment.
+
=back
=head1 METHODS
warn "$me upgrading $class\n" if $DEBUG;
+ local $FS::payinfo_Mixin::ignore_masked_payinfo = 1;
+
##
# otaker/ivan upgrade
##
if $error;
}
+ ###
+ # migrate gateway info from the misused 'paybatch' field
+ ###
+
+ # not only cust_pay, but also voided and refunded payments
+ if (!FS::upgrade_journal->is_done('cust_pay__parse_paybatch')) {
+ # really inefficient, but again, only has to run once
+ foreach my $table (qw(cust_pay cust_pay_void cust_refund)) {
+ foreach my $object ( qsearch({
+ table => $table,
+ extra_sql => "WHERE payby IN('CARD','CHEK') ".
+ "AND paybatch IS NOT NULL",
+ }) )
+ {
+ my $parsed = $object->_parse_paybatch;
+ if (keys %$parsed) {
+ $object->set($_ => $parsed->{$_}) foreach keys %$parsed;
+ $object->set('paybatch', '');
+ my $error = $object->replace;
+ warn "error parsing CARD/CHEK paybatch fields on $object #".
+ $object->get($object->primary_key).":\n $error\n"
+ if $error;
+ }
+ } #$object
+ } #$table
+ FS::upgrade_journal->set_done('cust_pay__parse_paybatch');
+ }
}
=back
package FS::cust_pay_void;
use strict;
-use base qw( FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
+use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin
FS::Record );
use vars qw( @encrypted_fields $otaker_upgrade_kludge );
use Business::CreditCard;
books closed flag, empty or `Y'
+=item gatewaynum, processor, auth, order_number
+
+Same as for L<FS::cust_pay>, but specifically the result of realtime
+authorization of the refund.
+
=back
=head1 METHODS
use FS::payby;
use FS::Record qw(qsearch);
+use vars qw($ignore_masked_payinfo);
+
=head1 NAME
FS::payinfo_Mixin - Mixin class for records in tables that contain payinfo.
if ( $self->payby eq 'CARD' && ! $self->is_encrypted($self->payinfo) ) {
my $payinfo = $self->payinfo;
- $payinfo =~ s/\D//g;
- $self->payinfo($payinfo);
- if ( $self->payinfo ) {
- $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
- or return "Illegal (mistyped?) credit card number (payinfo)";
- $self->payinfo($1);
- validate($self->payinfo) or return "Illegal credit card number";
- return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
- && cardtype($self->payinfo) eq "Unknown";
+ if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
+ # allow it
} else {
- $self->payinfo('N/A'); #???
+ $payinfo =~ s/\D//g;
+ $self->payinfo($payinfo);
+ if ( $self->payinfo ) {
+ $self->payinfo =~ /^(\d{13,16}|\d{8,9})$/
+ or return "Illegal (mistyped?) credit card number (payinfo)";
+ $self->payinfo($1);
+ validate($self->payinfo) or return "Illegal credit card number";
+ return "Unknown card type" if $self->payinfo !~ /^99\d{14}$/ #token
+ && cardtype($self->payinfo) eq "Unknown";
+ } else {
+ $self->payinfo('N/A'); #???
+ }
}
} else {
if ( $self->is_encrypted($self->payinfo) ) {
}
}
- '';
-
}
=item payby_payinfo_pretty
=head1 DESCRIPTION
This is a mixin class for records that represent transactions: that contain
-payinfo and paybatch. Currently FS::cust_pay and FS::cust_refund
+payinfo and realtime result fields (gatewaynum, processor, authorization,
+order_number). Currently FS::cust_pay, FS::cust_refund, and FS::cust_pay_void.
=head1 METHODS
}
}
-=item gatewaynum
+# We keep _parse_paybatch just because the upgrade needs it.
-Returns a gatewaynum for the processing gateway.
-
-=item processor
-
-Returns a name for the processing gateway.
-
-=item authorization
-
-Returns a name for the processing gateway.
-
-=item order_number
-
-Returns a name for the processing gateway.
-
-=cut
-
-sub gatewaynum { shift->_parse_paybatch->{'gatewaynum'}; }
-sub processor { shift->_parse_paybatch->{'processor'}; }
-sub authorization { shift->_parse_paybatch->{'authorization'}; }
-sub order_number { shift->_parse_paybatch->{'order_number'}; }
-
-#sucks that this stuff is in paybatch like this in the first place,
-#but at least other code can start to use new field names
-#(code nicked from FS::cust_main::realtime_refund_bop)
sub _parse_paybatch {
my $self = shift;
}
+# because we can't actually name the field 'authorization' (reserved word)
+sub authorization {
+ my $self = shift;
+ $self->auth(@_);
+}
+
+=item payinfo_check
+
+Checks the validity of the realtime payment fields (gatewaynum, processor,
+auth, and order_number) as well as payby and payinfo
+
+=cut
+
+sub payinfo_check {
+ my $self = shift;
+
+ # All of these can be null, so in principle this could go in payinfo_Mixin.
+
+ $self->SUPER::payinfo_check()
+ || $self->ut_numbern('gatewaynum')
+ # not ut_foreign_keyn, it causes upgrades to fail
+ || $self->ut_alphan('processor')
+ || $self->ut_textn('auth')
+ || $self->ut_textn('order_number')
+ || '';
+}
+
=back
=head1 SEE ALSO
use Text::CSV;
my %opt;
-getopts('va:P:C:T:', \%opt);
+getopts('va:P:C:e:', \%opt);
+
+# Product codes that are subject to flat rate E911 charges. For these
+# products, the'quantity' field represents the number of lines.
+my @E911_CODES = ( 'V-HPBX', 'V-TRUNK' );
+
+# Map TAXNONVOICE/TAXVOICE to Freeside taxclass names
+my %TAXCLASSES = (
+ 'TAXNONVOICE' => 'Other',
+ 'TAXVOICE' => 'VoIP',
+);
+
#$Net::SFTP::Foreign::debug = -1;
sub HELP_MESSAGE { '
[ -a archivedir ]
[ -P port ]
[ -C category ]
- [ -T taxclass ]
+ [ -e pkgpart ]
freesideuser sftpuser@hostname[:path]
' }
'custnum',
'date_desc',
'quantity',
- 'amount',
+ 'unit_price',
'classname',
+ 'taxclass',
);
my $user = shift or die &HELP_MESSAGE;
-adminsuidsetup $user;
+my $dbh = adminsuidsetup $user;
+$FS::UID::AutoCommit = 0;
# for statistics
my $num_charges = 0;
unless -w $opt{a};
}
+my $e911_part_pkg;
+if ( $opt{e} ) {
+ $e911_part_pkg = FS::part_pkg->by_key($opt{e})
+ or die "E911 pkgpart $opt{e} not found.\n";
+
+ if ( $e911_part_pkg->base_recur > 0 or $e911_part_pkg->freq ) {
+ die "E911 pkgpart $opt{e} must be a one-time charge.\n";
+ }
+}
+
my $categorynum = '';
if ( $opt{C} ) {
# find this category (don't auto-create it, it should exist already)
$categorynum = $category->categorynum;
}
-my $taxclass = $opt{T} || '';
-
#my $tmpdir = File::Temp->newdir();
my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere?
port => $port,
# for now we don't support passwords. use authorized_keys.
timeout => 30,
- more => ($opt{v} ? '-v' : ''),
+ #more => ($opt{v} ? '-v' : ''),
);
die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
if $sftp->error;
print STDERR "No charge files found.\n" if $opt{v};
exit(-1);
}
+
+my %cust_main; # cache
+my %e911_qty; # custnum => sum of E911-subject quantity
+
+my %is_e911 = map {$_ => 1} @E911_CODES;
+
FILE: foreach my $filename (@$files) {
print STDERR "Retrieving $filename\n" if $opt{v};
$sftp->get("$filename", "$tmpdir/$filename");
open my $fh, "<$tmpdir/$filename";
my $header = <$fh>;
- if ($header !~ /^cust_id/) {
+ if ($header !~ /^"cust_id"/) {
warn "warning: $filename has incorrect header row:\n$header\n";
# but try anyway
}
next FILE;
};
@hash{@fields} = $csv->fields();
- my $cust_main = FS::cust_main->by_key($hash{custnum});
+ my $cust_main =
+ $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
if (!$cust_main) {
warn "customer #$hash{custnum} not found\n";
next;
print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n"
if $opt{v};
+ my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
# construct arguments for $cust_main->charge
- my %opt = (
- amount => $hash{amount},
+ my %charge_opt = (
+ amount => $amount,
quantity => $hash{quantity},
start_date => $cust_main->next_bill_date,
pkg => $hash{date_desc},
- taxclass => $taxclass,
+ taxclass => $TAXCLASSES{ $hash{taxclass} },
);
if (my $classname = $hash{classname}) {
if (!exists($classnum_of{$classname}) ) {
$classnum_of{$classname} = $pkg_class->classnum;
}
- $opt{classnum} = $classnum_of{$classname};
+ $charge_opt{classnum} = $classnum_of{$classname};
}
- print STDERR " Charging $hash{amount}\n"
+ print STDERR " Charging $hash{unit_price} * $hash{quantity}\n"
if $opt{v};
- my $error = $cust_main->charge(\%opt);
+ my $error = $cust_main->charge(\%charge_opt);
if ($error) {
warn "Error creating charge: $error" if $error;
$num_errors++;
$num_charges++;
$sum_charges += $hash{amount};
}
+
+ if ( $opt{e} and $is_e911{$hash{classname}} ) {
+ $e911_qty{$hash{custnum}} ||= 0;
+ $e911_qty{$hash{custnum}} += $hash{quantity};
+ }
} #while $line
close $fh;
} #FILE
+# Order E911 packages
+my $num_e911 = 0;
+my $num_lines = 0;
+foreach my $custnum ( keys (%e911_qty) ) {
+ my $cust_main = $cust_main{$custnum};
+ my $quantity = $e911_qty{$custnum};
+ next if $quantity == 0;
+ my $cust_pkg = FS::cust_pkg->new({
+ pkgpart => $opt{e},
+ custnum => $custnum,
+ start_date => $cust_main->next_bill_date,
+ quantity => $quantity,
+ });
+ my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg });
+ if ( $error ) {
+ warn "Error creating e911 charge for customer $custnum: $error\n";
+ $num_errors++;
+ } else {
+ $num_e911++;
+ $num_lines += $quantity;
+ }
+}
+
+$dbh->commit;
+
if ($opt{v}) {
print STDERR "
Finished!
Processed files: @$files
Created charges: $num_charges
Sum of charges: \$".sprintf('%0.2f', $sum_charges)."
+ E911 charges: $num_e911
+ E911 lines: $num_lines
Errors: $num_errors
";
}
=head1 NAME
-freeside-eftca-download - Retrieve payment batch responses from EFT Canada.
+freeside-ipifony-download - Download and import invoice items from IPifony.
=head1 SYNOPSIS
- freeside-eftca-download [ -v ] [ -a archivedir ] user
+ freeside-ipifony-download
+ [ -v ]
+ [ -a archivedir ]
+ [ -P port ]
+ [ -C category ]
+ [ -T taxclass ]
+ [ -e pkgpart ]
+ freesideuser sftpuser@hostname[:path]
-=head1 DESCRIPTION
+=head1 REQUIRED PARAMETERS
-Command line tool to download returned payment reports from the EFT Canada
-gateway and void the returned payments. Uses the login and password from
-'batchconfig-eft_canada'.
+I<freesideuser>: the Freeside user to run as.
--v: Be verbose.
+I<sftpuser>: the SFTP user to connect as. The 'freeside' system user should
+have an authorization key to connect as that user.
--a directory: Archive response files in the provided directory.
+I<hostname>: the SFTP server.
+
+=head1 OPTIONAL PARAMETERS
+
+-v: Be verbose.
-user: freeside username
+-a I<archivedir>: Save a copy of the downloaded file to I<archivedir>.
-=head1 BUGS
+-P I<port>: Connect to that TCP port.
-You need to manually SFTP to ftp.eftcanada.com from the freeside account
-and accept their key before running this script.
+-C I<category>: The name of a package category to use when creating package
+classes.
-=head1 SEE ALSO
+-e I<pkgpart>: The pkgpart (L<FS::part_pkg>) to use for E911 charges. A
+package of this type will be ordered for each invoice that has E911-subject
+line items. The 'quantity' field on this package will be set to the total
+quantity of those line items.
-L<FS::pay_batch>
+The E911 package must be a one-time package (flat rate, no frequency, no
+recurring fee) with setup fee equal to the fee per line.
=cut
my $canceled = 0;
print "Voiding ".scalar(@auths)." transactions:\n" if $opt{'v'};
foreach my $authnum (@auths) {
- my $paybatch = $gatewaynum . $processor . ':' . $authnum;
- my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
+ my $cust_pay = qsearchs('cust_pay', {
+ gatewaynum => $gatewaynum,
+ processor => $processor,
+ authorization => $authnum,
+ });
my $error;
my $cancel_error;
if($cust_pay) {
}
}
else {
- my $cpv = qsearchs('cust_pay_void', { paybatch => $paybatch });
+ my $cpv = qsearchs('cust_pay_void', {
+ gatewaynum => $gatewaynum,
+ processor => $processor,
+ authorization => $authnum,
+ });
if($cpv) {
$error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) .
' by ' . $cpv->otaker;
install-texmf:
install -D -o freeside -m 444 etc/longtable.sty \
- ~freeside/texmf/tex/longtable.sty
- texhash ~freeside
+ /usr/local/share/texmf/tex/latex/longtable.sty
+ texhash /usr/local/share/texmf
install-init:
#[ -e ${INIT_FILE} ] || install -o root -g ${INSTALLGROUP} -m 711 init.d/freeside-init ${INIT_FILE}
</TR>
% }
-%
-% #false laziness w/FS/FS/cust_main::realtime_refund_bop
-% if ( $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/ ) {
-% my ( $processor, $auth, $order_number ) = ( $1, $2, $4 );
-%
-
-
<TR>
- <TD ALIGN="right">Processor</TD><TD BGCOLOR="#ffffff"><% $processor %></TD>
+ <TD ALIGN="right">Processor</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_pay->processor %></TD>
</TR>
% if ( length($auth) ) {
<TR>
- <TD ALIGN="right">Authorization</TD><TD BGCOLOR="#ffffff"><% $auth %></TD>
+ <TD ALIGN="right">Authorization</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_pay->auth %></TD>
</TR>
% }
-% if ( length($order_number) ) {
+% if ( length($cust_pay->order_number) ) {
<TR>
- <TD ALIGN="right">Order number</TD><TD BGCOLOR="#ffffff"><% $order_number %></TD>
+ <TD ALIGN="right">Order number</TD>
+ <TD BGCOLOR="#ffffff"><% $cust_pay->order_number %></TD>
</TR>
% }
-% }
+% } #if $cust_pay
</TABLE>
% }
bank depositor account teller
)
#} fields('cust_pay')
+ # gatewaynum, processor, auth, order_number
+ # are for realtime payments only, and can't be entered manually
} );
my @rights = ('Post payment');
'bottom_total' => 1,
'bottom_link' => $bottom_link,
'agentnum' => $agentnum,
+ 'cust_classnum'=> \@cust_classnums,
)
%>
<%init>
$title .= 'Sales Report (Gross)';
$title .= ', average per customer package' if $average_per_cust_pkg;
+my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+$bottom_link .= "cust_classnum=$_;" foreach @cust_classnums;
+
#classnum (here)
# 0: all classes
# not specified: empty class
push @links, "$link;".
($all_agent ? '' : "agentnum=$row_agentnum;").
($all_part_referral ? '' : "refnum=$row_refnum;").
+ (join('',map {"cust_classnum=$_;"} @cust_classnums)).
($all_class ? '' : "classnum=$row_classnum;").
"distribute=$distribute;".
"use_override=$use_override;charges=$component;";
my $component = join('', @components);
my @row_params = ( 'agentnum' => $row_agentnum,
+ 'cust_classnum' => \@cust_classnums,
'use_override' => $use_override,
'average_per_cust_pkg' => $average_per_cust_pkg,
'distribute' => $distribute,
$row_link .= ";refnum=".$sel_part_referral->refnum;
}
+ $row_link .= ";cust_classnum=$_" foreach @cust_classnums;
+
push @items, 'cust_bill_pkg';
push @labels, mt('[_1] - Subtotal', $agent->agent);
push @params, \@row_params;
#optional
'agentnum' => $agentnum,
'refnum' => $refnum,
+ 'cust_classnum' => \@classnums,
'nototal' => 1,
'graph_type' => 'LinesPoints',
'remove_empty' => 1,
'projection' => $opt{'projection'},
'agentnum' => $opt{'agentnum'},
'refnum' => $opt{'refnum'},
+ 'cust_classnum'=> $opt{'cust_classnum'},
'remove_empty' => $opt{'remove_empty'},
'doublemonths' => $opt{'doublemonths'},
);
'links' => \%link,
'agentnum' => $agentnum,
'refnum' => $refnum,
+ 'cust_classnum'=> \@classnums,
'nototal' => scalar($cgi->param('12mo')),
)
%>
}
my $agentname = $agent ? $agent->agent.' ' : '';
+my @classnums;
+if ( $cgi->param('cust_classnum') ) {
+ @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+}
+
my( $refnum, $part_referral ) = ('', '');
if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
$refnum = $1;
foreach keys %color;
my $ar = "agentnum=$agentnum;refnum=$refnum";
+$ar .= ";cust_classnum=$_" foreach @classnums;
my %link = (
'invoiced' => "${p}search/cust_bill.html?$ar;",
'colors' => \%color,
'links' => \%link,
'agentnum' => $agentnum,
+ 'cust_classnum'=> \@classnums,
'nototal' => scalar($cgi->param('12mo')),
'daily' => 1,
'start_day' => $smday,
my $agentname = $agent ? $agent->agent.' ' : '';
+my @classnums;
+if ( $cgi->param('cust_classnum') ) {
+ @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+}
+
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
my ($ssec,$smin,$shour,$smday,$smon,$syear,$swday,$syday,$sisdst)
= localtime($beginning);
'onchange' => 'enable_agent_totals',
&>
+<& /elements/tr-select-cust_class.html,
+ 'field' => 'cust_classnum',
+ 'label' => 'Customer class',
+ 'multiple' => 1,
+&>
+
<& /elements/tr-select-part_referral.html,
'field' => 'refnum',
'label' => 'Advertising source ',
)
%>
+<& /elements/tr-select-cust_class.html,
+ 'field' => 'cust_classnum', # to avoid ambiguity in FS::Report::Table
+ 'multiple' => 1
+&>
+
<% include('/elements/tr-select-part_referral.html',
'label' => 'Advertising source ',
'disable_empty' => 0,
)
%>
+<& /elements/tr-select-cust_class.html,
+ 'field' => 'cust_classnum',
+ 'multiple' => 1,
+&>
+
</TABLE>
<BR><INPUT TYPE="submit" VALUE="Display">
$search{'refnum'} = $1;
}
+ if ( $cgi->param('cust_classnum') ) {
+ $search{'cust_classnum'} = [ $cgi->param('cust_classnum') ];
+ }
+
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
$search{'custnum'} = $1;
}
$title = $part_referral->referral. " $title";
}
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
+
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
push @search, "cust_bill._date >= $beginning ",
"cust_bill._date <= $ending";
- refnum: Filter on customer reference source.
+- cust_classnum: Filter on customer class.
+
- classnum: Filter on package class.
- use_override: Apply "classnum" and "taxclass" filtering based on the
push @where, "cust_main.refnum = $1";
}
+# cust_classnum
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
# custnum
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.custnum = $1";
push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
}
+my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+if ( @cust_classnums ) {
+ push @where, 'cust_main.classnum IN ('.join(',', @cust_classnums).')';
+}
+
if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
push @where, "cust_main.agentnum = $1";
}
$title = $part_referral->referral. " $title";
}
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
if ( $unapplied ) {
push @search, FS::cust_credit->unapplied_sql . ' > 0';
}
$title = $part_referral->referral. " $title";
}
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
+
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
push @search, "cust_credit._date >= $beginning ",
"cust_credit._date <= $ending";
$title .= 'Customer Accounting Summary Report';
-my @custs = ();
-@custs = qsearch('cust_main', {} );
+my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
my @items = ('netsales', 'cashflow');
my @params = ( [], [] );
my $status = $cgi->param('status');
die "invalid status" unless $status =~ /^\w+|$/;
+my %search_hash;
+foreach (qw(agentnum refnum status)) {
+ if ( defined $cgi->param($_) ) {
+ $search_hash{$_} = $cgi->param($_);
+ }
+}
+$search_hash{'classnum'} = [ $cgi->param('cust_classnum') ]
+ if $cgi->param('cust_classnum');
+
+my $query = FS::cust_main::Search->search(\%search_hash);
+my @custs = qsearch($query);
+
foreach my $cust_main ( @custs ) {
# XXX should do this in the qsearch
next unless ($status eq '' || $status eq $cust_main->status);
$title = $part_referral->referral. " $title";
}
+ if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @search, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+ }
+
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
push @search, "custnum = $1";
}
push @search, "$table.payinfo = '$1'";
}
+ if ( $cgi->param('ccpay') =~ /^([\w-:]+)$/ ) {
+ # I think that's all the characters we need to allow.
+ # To avoid confusion, this parameter searches both auth and order_number.
+ push @search, "($table.auth LIKE '$1%') OR ($table.order_number LIKE '$1%')";
+ push @fields, 'auth', 'order_number';
+ push @header, 'Auth #', 'Transaction #';
+ $align .= 'rr';
+
+ }
+
if ( $cgi->param('usernum') =~ /^(\d+)$/ ) {
push @search, "$table.usernum = $1";
}
<SCRIPT TYPE="text/javascript">
function payby_changed(what) {
- if ( what.options[what.selectedIndex].value == 'BILL' ) {
- document.getElementById('checkno_caption').style.color = '#000000';
- what.form.payinfo.disabled = false;
- what.form.payinfo.style.backgroundColor = '#ffffff';
+ if ( what.value == 'BILL' ) {
+ show('payinfo');
+ hide('ccpay');
+ } else if ( what.value.match(/^CARD|CHEK/) ) {
+ hide('payinfo');
+ show('ccpay');
} else {
- document.getElementById('checkno_caption').style.color = '#bbbbbb';
- what.form.payinfo.disabled = true;
- what.form.payinfo.style.backgroundColor = '#dddddd';
+ hide('payinfo');
+ hide('ccpay');
}
}
+ function show(what) {
+ document.getElementById(what+'_caption').style.color = '#000000';
+ document.getElementById(what).disabled = false;
+ document.getElementById(what).style.backgroundColor = '#ffffff';
+ }
+
+ function hide(what) {
+ document.getElementById(what+'_caption').style.color = '#bbbbbb';
+ document.getElementById(what).disabled = true;
+ document.getElementById(what).style.backgroundColor = '#dddddd';
+ }
+
+
+
</SCRIPT>
<TR>
- <TD ALIGN="right"><FONT ID="checkno_caption" COLOR="#bbbbbb"><% mt('Check #:') |h %> </FONT></TD>
+ <TD ALIGN="right"><FONT ID="payinfo_caption" COLOR="#bbbbbb"><% mt('Check #:') |h %> </FONT></TD>
+ <TD>
+ <INPUT TYPE="text" ID="payinfo" NAME="payinfo" DISABLED STYLE="background-color: #dddddd">
+ </TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right">
+ <FONT ID="ccpay_caption" COLOR="#bbbbbb">
+ <% mt('Transaction #') |h %>
+ </FONT>
+ </TD>
<TD>
- <INPUT TYPE="text" NAME="payinfo" DISABLED STYLE="background-color: #dddddd">
+ <INPUT TYPE="text" ID="ccpay" NAME="ccpay" DISABLED STYLE="background-color: #dddddd">
</TD>
</TR>
push @where, FS::cust_main->cust_status_sql . " = '$status'";
}
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ $link .= ";cust_classnum=$_" foreach @classnums;
+ push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
my %total = ();
my %total_legacy = ();
foreach my $agentnum (@agentnums) {
label => emt('Customer status'),
&>
-<!-- customer
<& /elements/tr-select-cust_class.html,
- 'label' => emt('Class'),
+ 'label' => emt('Customer class'),
+ 'field' => 'cust_classnum',
'multiple' => 1,
'pre_options' => [ '' => emt('(none)') ],
'all_selected' => 1,
&>
--->
<& /elements/tr-input-beginning_ending.html &>
'disable_empty' => 1,
&>
+<& /elements/tr-select-cust_class.html,
+ 'field' => 'cust_classnum',
+ 'multiple' => 1,
+&>
+
<& /elements/tr-select-pkg_class.html,
'pre_options' => [ '' => 'all', '0' => '(empty class)' ],
'disable_empty' => 1,
'label' => 'Customer Status'
) %>
+ <& /elements/tr-select-cust_class.html,
+ 'label' => 'Customer Class',
+ 'field' => 'cust_classnum',
+ 'multiple' => 1,
+ &>
+
<& /elements/tr-checkbox.html,
'label' => 'Separate setup fees',
'field' => 'setuprecur',
<& /elements/tr-select-cust_main-status.html,
label => mt('Customer Status') &>
+ <& /elements/tr-select-cust_class.html,
+ label => mt('Customer Class'), field => 'cust_classnum', multiple => 1 &>
<& /elements/tr-select.html,
label => 'Invoice Status',
field => 'mode',
push @where, "cust_main.agentnum = $1";
}
+if ( $cgi->param('cust_classnum') ) {
+ my @classnums = grep /^\d+$/, $cgi->param('cust_classnum');
+ push @where, 'cust_main.classnum IN('.join(',',@classnums).')'
+ if @classnums;
+}
+
# no pkgclass, no taxclass, no tax location...
# unearned revenue mode
my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
-my $money_char;
+my $conf = new FS::Conf;
+my $money_char = $conf->config('money_char') || '$';
sub money_sub {
$conf ||= new FS::Conf;
<TD BGCOLOR="#FFFFFF"><B><% $cust_pay->payby_name %> #<% $cust_pay->paymask %></B></TD>
</TR>
-% if ( $cust_pay->payby =~ /^(CARD|CHEK|LECB)$/ && $cust_pay->paybatch ) {
+% if ( $cust_pay->payby =~ /^(CARD|CHEK|LECB)$/ && $cust_pay->processor ) {
<TR>
<TD ALIGN="right"><% mt('Processor') |h %></TD>
<TR>
<TD ALIGN="right"><% mt('Authorization #') |h %></TD>
- <TD BGCOLOR="#FFFFFF"><B><% $cust_pay->authorization %></B></TD>
+ <TD BGCOLOR="#FFFFFF"><B><% $cust_pay->auth %></B></TD>
</TR>
% if ( $cust_pay->order_number ) {
<TD BGCOLOR="#FFFFFF"><B><% $cust_refund->payby_name %><% $cust_refund->paymask ? ' #'.$cust_refund->paymask : '' %></B></TD>
</TR>
-% if ( $cust_refund->payby =~ /^(CARD|CHEK|LECB)$/ && $cust_refund->paybatch ) {
+% if ( $cust_refund->payby =~ /^(CARD|CHEK|LECB)$/ && $cust_refund->processor ) {
<TR>
<TD ALIGN="right"><% mt('Processor') |h %></TD>
<TR>
<TD ALIGN="right"><% mt('Authorization #') |h %></TD>
- <TD BGCOLOR="#FFFFFF"><B><% $cust_refund->authorization %></B></TD>
+ <TD BGCOLOR="#FFFFFF"><B><% $cust_refund->auth %></B></TD>
</TR>
% if ( $cust_refund->order_number ) {
sub Prepare {
my $self = shift;
my $cfname = $self->Argument or return 0;
- $self->{'inc_by'} = $self->TransactionObj->FirstCustomFieldValue($cfname)
- || '';
- return ( $self->{'inc_by'} =~ /^(\d+)$/ );
+ #RT::Logger->info('Accumulate::Prepare called on transaction '.
+ # $self->TransactionObj->Id." field $cfname");
+ my $TransObj = $self->TransactionObj;
+ my $TicketObj = $self->TicketObj;
+ if ( $TransObj->Type eq 'Create' and
+ !defined($TransObj->FirstCustomFieldValue($cfname)) ) {
+ # special case: we're creating a new ticket, and the initial value
+ # may have been set on the ticket instead of the transaction, so
+ # update the transaction to match
+ $self->{'obj'} = $TransObj;
+ $self->{'inc_by'} = $TicketObj->FirstCustomFieldValue($cfname);
+ } else {
+ # the usual case when updating an existing ticket
+ $self->{'obj'} = $TicketObj;
+ $self->{'inc_by'} = $TransObj->FirstCustomFieldValue($cfname)
+ || '';
+ }
+ return ( $self->{'inc_by'} =~ /^(\d+)$/ ); # else it's empty
}
sub Commit {
my $self = shift;
my $cfname = $self->Argument;
+ my $obj = $self->{'obj'};
my $newval = $self->{'inc_by'} +
- ($self->TicketObj->FirstCustomFieldValue($cfname) || 0);
- my ($val) = $self->TicketObj->AddCustomFieldValue(
- Field => 'Support time',
- Value => $newval,
- RecordTransaction => 0,
+ ($obj->FirstCustomFieldValue($cfname) || 0);
+ #RT::Logger->info('Accumulate::Commit called on '.ref($obj).' '.
+ # $obj->Id." field $cfname");
+ my ($val) = $obj->AddCustomFieldValue(
+ Field => $cfname,
+ Value => $newval,
+ RecordTransaction => 0,
);
return $val;
}
}
-sub _FreesideURILabelLong {
+sub AsStringLong {
my $self = shift;
} elsif ( $table eq 'cust_svc' ) {
my $string = '';
- # we now do this within the UI
- #my $cust = $self->CustomerResolver;
- #if ( $cust ) {
- # $string = $cust->AsStringLong;
- #}
- $string .= $self->AsString;
+ my $cust = $self->CustomerResolver;
+ if ( $cust ) {
+ # the customer's small_custview
+ $string = $cust->AsStringLong();
+ }
+ # + the service label and link
+ $string .= $self->ShortLink;
return $string;
} else {
- return $self->_FreesideURILabel();
+ return $self->SUPER::AsStringLong;
}
}
-sub AsString {
+sub ShortLink {
+ # because I don't want AsString to sometimes return a hunk of HTML, but
+ # on the other hand AsStringLong does something specific.
my $self = shift;
- if ( $self->{'fstable'} eq 'cust_svc' ) {
- return '<B><A HREF="' . $self->HREF . '">' .
- $self->_FreesideURILabel . '</A></B>';
- } else {
- $self->SUPER::AsString;
- }
+ '<B><A HREF="'.$self->HREF.'">' . $self->_FreesideURILabel . '</A></B>';
}
sub CustomerResolver {
% }
</td>
<td>
+% if ( $resolver->URI =~ /cust_main/ ) {
<% $resolver->AsStringLong |n %>
+% } elsif ( $resolver->URI =~ /cust_svc/ ) {
+ <% $resolver->ShortLink |n %>
+% }
</td>
</tr>
% }
<td class="value">
<% $cust->AsStringLong |n %>
% foreach my $svc ( @{ $data{cust_svc}{$custnum} || [] } ) {
- <% $svc->AsString |n %>
+ <% $svc->ShortLink |n %>
<br>
% }
</td>