diff options
49 files changed, 935 insertions, 436 deletions
diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm index 696940679..2e202e5d9 100644 --- a/FS/FS/Report/Table.pm +++ b/FS/FS/Report/Table.pm @@ -56,6 +56,13 @@ sub signups { 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) ); @@ -439,8 +446,16 @@ sub cust_bill_pkg_setup { $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 @@ -463,6 +478,13 @@ sub cust_bill_pkg_recur { 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'} ) { @@ -518,6 +540,13 @@ sub cust_bill_pkg_detail { 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, @@ -657,6 +686,14 @@ sub for_opts { 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; } diff --git a/FS/FS/Report/Table/Monthly.pm b/FS/FS/Report/Table/Monthly.pm index ee4dc5fe8..b8e52ae63 100644 --- a/FS/FS/Report/Table/Monthly.pm +++ b/FS/FS/Report/Table/Monthly.pm @@ -25,6 +25,7 @@ FS::Report::Table::Monthly - Tables of report data, indexed monthly #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 @@ -69,6 +70,9 @@ corresponding to this arrayref. =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 @@ -139,6 +143,8 @@ sub data { my $agentnum = $self->{'agentnum'}; my $refnum = $self->{'refnum'}; + my $cust_classnum = $self->{'cust_classnum'} || []; + $cust_classnum = [ $cust_classnum ] if !ref($cust_classnum); if ( $projecting ) { @@ -183,6 +189,7 @@ sub data { 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; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index b6fd3b67b..cbcd27b46 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1560,7 +1560,14 @@ sub tables_hashref { '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' ] ], @@ -1591,6 +1598,12 @@ sub tables_hashref { '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, '', '', @@ -1858,6 +1871,11 @@ sub tables_hashref { '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' => [], diff --git a/FS/FS/TicketSystem.pm b/FS/FS/TicketSystem.pm index c1553f17a..165856e5f 100644 --- a/FS/FS/TicketSystem.pm +++ b/FS/FS/TicketSystem.pm @@ -87,6 +87,8 @@ sub _upgrade_data { # 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); @@ -252,6 +254,71 @@ sub _upgrade_data { 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; } diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm index 01e2e2966..665c16692 100644 --- a/FS/FS/TicketSystem/RT_Internal.pm +++ b/FS/FS/TicketSystem/RT_Internal.pm @@ -589,7 +589,7 @@ sub _web_external_auth { # 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 ) ); diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index e4b2df4e8..e7622d712 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -3429,6 +3429,15 @@ sub search_sql_where { 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}}; diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 5cff140bb..716c0983e 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -201,16 +201,50 @@ sub insert { 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 ) { diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 4ea4a6b9d..45d57cd79 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3403,6 +3403,8 @@ New-style, with a hashref of options: 'setuptax' => '', # or 'Y' for tax exempt + 'locationnum'=> 1234, # optional + #internal taxation 'taxclass' => 'Tax class', @@ -3434,6 +3436,7 @@ sub charge { 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; @@ -3451,6 +3454,7 @@ sub charge { $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; @@ -3517,6 +3521,7 @@ sub charge { 'quantity' => $quantity, 'start_date' => $start_date, 'no_auto' => $no_auto, + 'locationnum'=> $locationnum, } ); $error = $cust_pkg->insert; diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 6d3ff9146..cd46c7332 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -687,8 +687,6 @@ sub _omit_zero_value_bundles { =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>. @@ -755,7 +753,7 @@ sub calculate_taxes { # 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; @@ -775,45 +773,35 @@ sub calculate_taxes { # 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 } }, @@ -823,56 +811,53 @@ sub calculate_taxes { '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' => '', @@ -890,19 +875,9 @@ sub calculate_taxes { 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; @@ -1184,11 +1159,23 @@ sub _make_lines { # 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; @@ -1200,6 +1187,25 @@ sub _make_lines { } +# 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; @@ -1212,175 +1218,152 @@ sub _handle_taxes { 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; } } - ''; } diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 80063debb..804969b16 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -757,19 +757,6 @@ sub fake_bop { 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'}, @@ -778,9 +765,11 @@ sub fake_bop { '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}); @@ -841,17 +830,8 @@ sub _realtime_bop_result { 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, @@ -860,10 +840,14 @@ sub _realtime_bop_result { '_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} ) @@ -1363,6 +1347,7 @@ sub realtime_refund_bop { my( $processor, $login, $password, @bop_options, $namespace ) ; my( $auth, $order_number ) = ( '', '', '' ); + my $gatewaynum = ''; if ( $options{'paynum'} ) { @@ -1371,11 +1356,22 @@ sub realtime_refund_bop { 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 @@ -1605,9 +1601,7 @@ sub realtime_refund_bop { 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; @@ -1624,8 +1618,11 @@ sub realtime_refund_bop { '_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 ) { diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm index 87c1ca730..db6be751d 100644 --- a/FS/FS/cust_main_county.pm +++ b/FS/FS/cust_main_county.pm @@ -258,10 +258,15 @@ sub _list_sql { =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. @@ -275,8 +280,7 @@ tax exemption limit if there is one. =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 ) = @_; @@ -294,7 +298,8 @@ sub taxline { 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'}; @@ -325,6 +330,15 @@ sub taxline { 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; @@ -472,37 +486,47 @@ sub taxline { $_->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 diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index d28997ccd..4535aadb2 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -100,7 +100,7 @@ Masked payinfo (See L<FS::payinfo_Mixin> for how this works) =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 @@ -130,11 +130,32 @@ The deposit account number. 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 @@ -878,6 +899,8 @@ sub _upgrade_data { #class method warn "$me upgrading $class\n" if $DEBUG; + local $FS::payinfo_Mixin::ignore_masked_payinfo = 1; + ## # otaker/ivan upgrade ## @@ -1004,6 +1027,33 @@ sub _upgrade_data { #class method 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 diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm index bebcfd4cc..42fc2966c 100644 --- a/FS/FS/cust_pay_void.pm +++ b/FS/FS/cust_pay_void.pm @@ -1,7 +1,7 @@ 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; diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm index 7df7a557a..45a170ba0 100644 --- a/FS/FS/cust_refund.pm +++ b/FS/FS/cust_refund.pm @@ -87,6 +87,11 @@ order taker (see L<FS::access_user> 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 diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm index 7b713efd3..9879a3abd 100644 --- a/FS/FS/payinfo_Mixin.pm +++ b/FS/FS/payinfo_Mixin.pm @@ -5,6 +5,8 @@ use Business::CreditCard; 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. @@ -207,17 +209,21 @@ sub payinfo_check { 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) ) { @@ -230,8 +236,6 @@ sub payinfo_check { } } - ''; - } =item payby_payinfo_pretty diff --git a/FS/FS/payinfo_transaction_Mixin.pm b/FS/FS/payinfo_transaction_Mixin.pm index 19419de1c..093891e93 100644 --- a/FS/FS/payinfo_transaction_Mixin.pm +++ b/FS/FS/payinfo_transaction_Mixin.pm @@ -23,7 +23,8 @@ use vars qw(@ISA); =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 @@ -55,32 +56,8 @@ sub payby_name { } } -=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; @@ -112,6 +89,33 @@ sub _parse_paybatch { } +# 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 diff --git a/FS/bin/freeside-ipifony-download b/FS/bin/freeside-ipifony-download index 64905e193..837cc3329 100644 --- a/FS/bin/freeside-ipifony-download +++ b/FS/bin/freeside-ipifony-download @@ -12,7 +12,18 @@ use FS::Conf; 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 { ' @@ -22,7 +33,7 @@ sub HELP_MESSAGE { ' [ -a archivedir ] [ -P port ] [ -C category ] - [ -T taxclass ] + [ -e pkgpart ] freesideuser sftpuser@hostname[:path] ' } @@ -30,12 +41,14 @@ my @fields = ( '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; @@ -51,6 +64,16 @@ if ( $opt{a} ) { 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) @@ -61,8 +84,6 @@ if ( $opt{C} ) { $categorynum = $category->categorynum; } -my $taxclass = $opt{T} || ''; - #my $tmpdir = File::Temp->newdir(); my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere? @@ -88,7 +109,7 @@ my $sftp = Net::SFTP::Foreign->new( 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; @@ -100,6 +121,12 @@ if (!@$files) { 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"); @@ -133,7 +160,7 @@ FILE: foreach my $filename (@$files) { 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 } @@ -145,7 +172,8 @@ FILE: foreach my $filename (@$files) { 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; @@ -153,13 +181,14 @@ FILE: foreach my $filename (@$files) { 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}) ) { @@ -182,11 +211,11 @@ FILE: foreach my $filename (@$files) { $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++; @@ -194,48 +223,94 @@ FILE: foreach my $filename (@$files) { $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 diff --git a/FS/bin/freeside-void-payments b/FS/bin/freeside-void-payments index 8c1f3dbdf..49b74d388 100755 --- a/FS/bin/freeside-void-payments +++ b/FS/bin/freeside-void-payments @@ -90,8 +90,11 @@ my $notfound = 0; 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) { @@ -103,7 +106,11 @@ foreach my $authnum (@auths) { } } 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; @@ -248,8 +248,8 @@ dev-perl-modules: perl-modules 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} diff --git a/httemplate/edit/cust_refund.cgi b/httemplate/edit/cust_refund.cgi index 1ef69fdae..656d5ebb5 100755 --- a/httemplate/edit/cust_refund.cgi +++ b/httemplate/edit/cust_refund.cgi @@ -60,29 +60,25 @@ </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> % } diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi index ce0ec3212..a002fa181 100755 --- a/httemplate/edit/process/cust_pay.cgi +++ b/httemplate/edit/process/cust_pay.cgi @@ -57,6 +57,8 @@ my $new = new FS::cust_pay ( { 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'); diff --git a/httemplate/graph/cust_bill_pkg.cgi b/httemplate/graph/cust_bill_pkg.cgi index c334ae9e7..91bedf3fe 100644 --- a/httemplate/graph/cust_bill_pkg.cgi +++ b/httemplate/graph/cust_bill_pkg.cgi @@ -13,6 +13,7 @@ 'bottom_total' => 1, 'bottom_link' => $bottom_link, 'agentnum' => $agentnum, + 'cust_classnum'=> \@cust_classnums, ) %> <%init> @@ -68,6 +69,9 @@ $title .= $sel_part_referral->referral.' ' $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 @@ -188,6 +192,7 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => 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;"; @@ -209,6 +214,7 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => 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, @@ -231,6 +237,8 @@ foreach my $agent ( $all_agent || $sel_agent || qsearch('agent', { 'disabled' => $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; diff --git a/httemplate/graph/elements/monthly.html b/httemplate/graph/elements/monthly.html index c736de696..939f18a35 100644 --- a/httemplate/graph/elements/monthly.html +++ b/httemplate/graph/elements/monthly.html @@ -37,6 +37,7 @@ Example: #optional 'agentnum' => $agentnum, 'refnum' => $refnum, + 'cust_classnum' => \@classnums, 'nototal' => 1, 'graph_type' => 'LinesPoints', 'remove_empty' => 1, @@ -121,6 +122,7 @@ my %reportopts = ( 'projection' => $opt{'projection'}, 'agentnum' => $opt{'agentnum'}, 'refnum' => $opt{'refnum'}, + 'cust_classnum'=> $opt{'cust_classnum'}, 'remove_empty' => $opt{'remove_empty'}, 'doublemonths' => $opt{'doublemonths'}, ); diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi index 166735fc6..9071fc7b9 100644 --- a/httemplate/graph/money_time.cgi +++ b/httemplate/graph/money_time.cgi @@ -8,6 +8,7 @@ 'links' => \%link, 'agentnum' => $agentnum, 'refnum' => $refnum, + 'cust_classnum'=> \@classnums, 'nototal' => scalar($cgi->param('12mo')), ) %> @@ -25,6 +26,11 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { } 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; @@ -93,6 +99,7 @@ $color{$_.'_12mo'} = $color{$_} foreach keys %color; my $ar = "agentnum=$agentnum;refnum=$refnum"; +$ar .= ";cust_classnum=$_" foreach @classnums; my %link = ( 'invoiced' => "${p}search/cust_bill.html?$ar;", diff --git a/httemplate/graph/money_time_daily.cgi b/httemplate/graph/money_time_daily.cgi index 4d16ff871..0fdbd895e 100644 --- a/httemplate/graph/money_time_daily.cgi +++ b/httemplate/graph/money_time_daily.cgi @@ -7,6 +7,7 @@ 'colors' => \%color, 'links' => \%link, 'agentnum' => $agentnum, + 'cust_classnum'=> \@classnums, 'nototal' => scalar($cgi->param('12mo')), 'daily' => 1, 'start_day' => $smday, @@ -32,6 +33,11 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { 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); diff --git a/httemplate/graph/report_cust_bill_pkg.html b/httemplate/graph/report_cust_bill_pkg.html index 31792e8dd..251e7d36e 100644 --- a/httemplate/graph/report_cust_bill_pkg.html +++ b/httemplate/graph/report_cust_bill_pkg.html @@ -34,6 +34,12 @@ function enable_agent_totals(obj) { '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 ', diff --git a/httemplate/graph/report_money_time.html b/httemplate/graph/report_money_time.html index 97876c996..315d31bc5 100644 --- a/httemplate/graph/report_money_time.html +++ b/httemplate/graph/report_money_time.html @@ -24,6 +24,11 @@ ) %> +<& /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, diff --git a/httemplate/graph/report_money_time_daily.html b/httemplate/graph/report_money_time_daily.html index 8328199c7..a436d0879 100644 --- a/httemplate/graph/report_money_time_daily.html +++ b/httemplate/graph/report_money_time_daily.html @@ -17,6 +17,11 @@ ) %> +<& /elements/tr-select-cust_class.html, + 'field' => 'cust_classnum', + 'multiple' => 1, +&> + </TABLE> <BR><INPUT TYPE="submit" VALUE="Display"> diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html index 406486a85..3c0530e4f 100755 --- a/httemplate/search/cust_bill.html +++ b/httemplate/search/cust_bill.html @@ -97,6 +97,10 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) { $search{'refnum'} = $1; } + if ( $cgi->param('cust_classnum') ) { + $search{'cust_classnum'} = [ $cgi->param('cust_classnum') ]; + } + if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { $search{'custnum'} = $1; } diff --git a/httemplate/search/cust_bill_pay.html b/httemplate/search/cust_bill_pay.html index 22e9a6795..79de74985 100644 --- a/httemplate/search/cust_bill_pay.html +++ b/httemplate/search/cust_bill_pay.html @@ -99,6 +99,13 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) { $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"; diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi index 817238da0..1e67e9320 100644 --- a/httemplate/search/cust_bill_pkg.cgi +++ b/httemplate/search/cust_bill_pkg.cgi @@ -120,6 +120,8 @@ Filtering parameters: - 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 @@ -258,6 +260,13 @@ if ( $cgi->param('refnum') =~ /^(\d+)$/ ) { 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"; diff --git a/httemplate/search/cust_bill_pkg_referral.html b/httemplate/search/cust_bill_pkg_referral.html index 77b486021..1289ff7ee 100644 --- a/httemplate/search/cust_bill_pkg_referral.html +++ b/httemplate/search/cust_bill_pkg_referral.html @@ -156,6 +156,11 @@ if ( @refnum ) { 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"; } diff --git a/httemplate/search/cust_credit.html b/httemplate/search/cust_credit.html index 38f03491d..f5d8fa19f 100755 --- a/httemplate/search/cust_credit.html +++ b/httemplate/search/cust_credit.html @@ -103,6 +103,12 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) { $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'; } diff --git a/httemplate/search/cust_credit_refund.html b/httemplate/search/cust_credit_refund.html index 361c8ad2f..75138e99d 100644 --- a/httemplate/search/cust_credit_refund.html +++ b/httemplate/search/cust_credit_refund.html @@ -85,6 +85,13 @@ if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) { $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"; diff --git a/httemplate/search/customer_accounting_summary.html b/httemplate/search/customer_accounting_summary.html index 0e9e24fa6..12c896276 100644 --- a/httemplate/search/customer_accounting_summary.html +++ b/httemplate/search/customer_accounting_summary.html @@ -142,8 +142,7 @@ $title .= $sel_part_referral->referral.' ' $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 = ( [], [] ); @@ -167,6 +166,18 @@ my @custnames = (); 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); diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index c60411107..eeef0c0e1 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -239,6 +239,12 @@ if ( $cgi->param('magic') ) { $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"; } @@ -324,6 +330,16 @@ if ( $cgi->param('magic') ) { 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"; } diff --git a/httemplate/search/elements/report_cust_pay_or_refund.html b/httemplate/search/elements/report_cust_pay_or_refund.html index a2b90b47d..0e04ab0dd 100644 --- a/httemplate/search/elements/report_cust_pay_or_refund.html +++ b/httemplate/search/elements/report_cust_pay_or_refund.html @@ -50,23 +50,48 @@ Examples: <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> diff --git a/httemplate/search/prepaid_income.html b/httemplate/search/prepaid_income.html index ebac5a2a9..03d121d70 100644 --- a/httemplate/search/prepaid_income.html +++ b/httemplate/search/prepaid_income.html @@ -129,6 +129,13 @@ if ( $cgi->param('status') =~ /^([a-z]+)$/ ) { 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) { diff --git a/httemplate/search/report_cust_bill_pkg.html b/httemplate/search/report_cust_bill_pkg.html index 4f6ee78db..f121ef49f 100644 --- a/httemplate/search/report_cust_bill_pkg.html +++ b/httemplate/search/report_cust_bill_pkg.html @@ -15,14 +15,13 @@ 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 &> diff --git a/httemplate/search/report_cust_bill_pkg_referral.html b/httemplate/search/report_cust_bill_pkg_referral.html index b4716d4fc..47478aa42 100644 --- a/httemplate/search/report_cust_bill_pkg_referral.html +++ b/httemplate/search/report_cust_bill_pkg_referral.html @@ -23,6 +23,11 @@ '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, diff --git a/httemplate/search/report_customer_accounting_summary.html b/httemplate/search/report_customer_accounting_summary.html index 537abffeb..8206f34ca 100755 --- a/httemplate/search/report_customer_accounting_summary.html +++ b/httemplate/search/report_customer_accounting_summary.html @@ -25,6 +25,12 @@ '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', diff --git a/httemplate/search/report_prepaid_income.html b/httemplate/search/report_prepaid_income.html index 4743e2d21..dfb2ea249 100644 --- a/httemplate/search/report_prepaid_income.html +++ b/httemplate/search/report_prepaid_income.html @@ -33,6 +33,8 @@ <& /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', diff --git a/httemplate/search/unearned_detail.html b/httemplate/search/unearned_detail.html index 02d514cbe..f61de052e 100644 --- a/httemplate/search/unearned_detail.html +++ b/httemplate/search/unearned_detail.html @@ -118,6 +118,12 @@ if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) { 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 @@ -235,7 +241,8 @@ my $query = { 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; diff --git a/httemplate/view/cust_pay.html b/httemplate/view/cust_pay.html index f9c8bc19c..76a24884a 100644 --- a/httemplate/view/cust_pay.html +++ b/httemplate/view/cust_pay.html @@ -77,7 +77,7 @@ <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> @@ -86,7 +86,7 @@ <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 ) { diff --git a/httemplate/view/cust_refund.html b/httemplate/view/cust_refund.html index 996b4c05a..319761506 100644 --- a/httemplate/view/cust_refund.html +++ b/httemplate/view/cust_refund.html @@ -62,7 +62,7 @@ <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> @@ -71,7 +71,7 @@ <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 ) { diff --git a/rt/lib/RT/Action/Accumulate.pm b/rt/lib/RT/Action/Accumulate.pm index 14675b894..0da7d2ef5 100644 --- a/rt/lib/RT/Action/Accumulate.pm +++ b/rt/lib/RT/Action/Accumulate.pm @@ -23,20 +23,38 @@ the same name, and should be single-valued fields. sub Prepare { my $self = shift; my $cfname = $self->Argument or return 0; - $self->{'inc_by'} = $self->TransactionObj->FirstCustomFieldValue($cfname) - || ''; - 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; } diff --git a/rt/lib/RT/URI/freeside/Internal.pm b/rt/lib/RT/URI/freeside/Internal.pm index 61b256ef9..b0962860d 100644 --- a/rt/lib/RT/URI/freeside/Internal.pm +++ b/rt/lib/RT/URI/freeside/Internal.pm @@ -143,7 +143,7 @@ sub small_custview { } -sub _FreesideURILabelLong { +sub AsStringLong { my $self = shift; @@ -161,30 +161,28 @@ sub _FreesideURILabelLong { } 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 { diff --git a/rt/share/html/Ticket/Elements/EditCustomers b/rt/share/html/Ticket/Elements/EditCustomers index e8aa69edc..cc9956f91 100644 --- a/rt/share/html/Ticket/Elements/EditCustomers +++ b/rt/share/html/Ticket/Elements/EditCustomers @@ -38,7 +38,11 @@ % } </td> <td> +% if ( $resolver->URI =~ /cust_main/ ) { <% $resolver->AsStringLong |n %> +% } elsif ( $resolver->URI =~ /cust_svc/ ) { + <% $resolver->ShortLink |n %> +% } </td> </tr> % } diff --git a/rt/share/html/Ticket/Elements/ShowCustomers b/rt/share/html/Ticket/Elements/ShowCustomers index 175822f52..f9b0133b5 100644 --- a/rt/share/html/Ticket/Elements/ShowCustomers +++ b/rt/share/html/Ticket/Elements/ShowCustomers @@ -24,7 +24,7 @@ my %data = $m->comp('Customers', Ticket => $Ticket); <td class="value"> <% $cust->AsStringLong |n %> % foreach my $svc ( @{ $data{cust_svc}{$custnum} || [] } ) { - <% $svc->AsString |n %> + <% $svc->ShortLink |n %> <br> % } </td> |