+ || $self->ut_textn('taxname')
+ || $self->ut_enum('setuptax', [ '', 'Y' ] )
+ || $self->ut_enum('recurtax', [ '', 'Y' ] )
+ || $self->SUPER::check
+ ;
+
+}
+
+=item label OPTIONS
+
+Returns a label looking like "Anytown, Alameda County, CA, US".
+
+If the taxname field is set, it will look like
+"CA Sales Tax (Anytown, Alameda County, CA, US)".
+
+If the taxclass is set, then it will be
+"Anytown, Alameda County, CA, US (International)".
+
+OPTIONS may contain "with_taxclass", "with_city", and "with_district" to show
+those fields. It may also contain "out", in which case, if this region
+(district+city+county+state+country) contains no non-zero taxes, the label
+will read "Out of taxable region(s)".
+
+=cut
+
+sub label {
+ my ($self, %opt) = @_;
+ if ( $opt{'out'}
+ and $self->tax == 0
+ and !defined(qsearchs('cust_main_county', {
+ 'district' => $self->district,
+ 'city' => $self->city,
+ 'county' => $self->county,
+ 'state' => $self->state,
+ 'country' => $self->country,
+ 'tax' => { op => '>', value => 0 },
+ })) )
+ {
+ return 'Out of taxable region(s)';
+ }
+ my $label = $self->country;
+ $label = $self->state.", $label" if $self->state;
+ $label = $self->county." County, $label" if $self->county;
+ if ($opt{with_city}) {
+ $label = $self->city.", $label" if $self->city;
+ if ($opt{with_district} and $self->district) {
+ $label = $self->district . ", $label";
+ }
+ }
+ # ugly labels when taxclass and taxname are both non-null...
+ # but this is how the tax report does it
+ if ($opt{with_taxclass}) {
+ $label = "$label (".$self->taxclass.')' if $self->taxclass;
+ }
+ $label = $self->taxname." ($label)" if $self->taxname;
+
+ $label;
+}
+
+=item sql_taxclass_sameregion
+
+Returns an SQL WHERE fragment or the empty string to search for entries
+with different tax classes.
+
+=cut
+
+#hmm, description above could be better...
+
+sub sql_taxclass_sameregion {
+ my $self = shift;
+
+ my $same_query = 'SELECT DISTINCT taxclass FROM cust_main_county '.
+ ' WHERE taxnum != ? AND country = ?';
+ my @same_param = ( 'taxnum', 'country' );
+ foreach my $opt_field (qw( state county )) {
+ if ( $self->$opt_field() ) {
+ $same_query .= " AND $opt_field = ?";
+ push @same_param, $opt_field;
+ } else {
+ $same_query .= " AND $opt_field IS NULL";
+ }
+ }
+
+ my @taxclasses = $self->_list_sql( \@same_param, $same_query );
+
+ return '' unless scalar(@taxclasses);
+
+ '( taxclass IS NULL OR ( '. #only if !$self->taxclass ??
+ join(' AND ', map { 'taxclass != '.dbh->quote($_) } @taxclasses ).
+ ' ) ) ';
+}
+
+sub _list_sql {
+ my( $self, $param, $sql ) = @_;
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute( map $self->$_(), @$param )
+ or die "Unexpected error executing statement $sql: ". $sth->errstr;
+ map $_->[0], @{ $sth->fetchall_arrayref };
+}
+
+=item taxline TAXABLES_ARRAYREF, [ OPTION => VALUE ... ]
+
+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.
+
+Options may include 'custnum' and 'invoice_date' in case the cust_bill_pkg
+objects belong to an invoice that hasn't been inserted yet.
+
+Options may include 'exemptions', an arrayref of L<FS::cust_tax_exempt_pkg>
+objects belonging to the same customer, to be counted against the monthly
+tax exemption limit if there is one.
+
+=cut
+
+# XXX change tax_rate.pm to work like this
+
+sub taxline {
+ my( $self, $taxables, %opt ) = @_;
+ return 'taxline called with no line items' unless @$taxables;
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $name = $self->taxname || 'Tax';
+ my $taxable_cents = 0;
+ my $tax_cents = 0;
+
+ my $cust_bill = $taxables->[0]->cust_bill;
+ my $custnum = $cust_bill ? $cust_bill->custnum : $opt{'custnum'};
+ my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{'invoice_date'};
+ my $cust_main = FS::cust_main->by_key($custnum) if $custnum > 0;
+ if (!$cust_main) {
+ # better way to handle this? should we just assume that it's taxable?
+ die "unable to calculate taxes for an unknown customer\n";
+ }
+
+ # set a flag if the customer is tax-exempt
+ my $exempt_cust;
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('cust_class-tax_exempt') ) {
+ my $cust_class = $cust_main->cust_class;
+ $exempt_cust = $cust_class->tax if $cust_class;
+ } else {
+ $exempt_cust = $cust_main->tax;
+ }
+
+ # set a flag if the customer is exempt from this tax here
+ my $exempt_cust_taxname = $cust_main->tax_exemption($self->taxname)
+ if $self->taxname;
+
+ # Gather any exemptions that are already attached to these cust_bill_pkgs
+ # so that we can deduct them from the customer's monthly limit.
+ my @existing_exemptions = @{ $opt{'exemptions'} };
+ 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;
+ my $part_pkg = $cust_bill_pkg->part_pkg;