+ #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 ) {
+ foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
+ next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen
+
+ push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ splice( @{ $_->_cust_tax_exempt_pkg } );
+ }
+ }
+
+ #some taxes are taxed
+ my %totlisthash;
+
+ warn "finding taxed taxes...\n" if $DEBUG > 2;
+ foreach my $tax ( keys %taxlisthash ) {
+ my $tax_object = shift @{ $taxlisthash{$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( $taxlisthash{ $totname } ); # only increase
+ # existing taxes
+ warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
+ if ( exists( $totlisthash{ $totname } ) ) {
+ push @{ $totlisthash{ $totname } }, $tax{ $tax };
+ }else{
+ $totlisthash{ $totname } = [ $tot, $tax{ $tax } ];
+ }
+ }
+ }
+
+ warn "having a look at taxed taxes...\n" if $DEBUG > 2;
+ foreach my $tax ( keys %totlisthash ) {
+ my $tax_object = shift @{ $totlisthash{$tax} };
+ warn "found previously found taxed tax ". $tax_object->taxname. "\n"
+ if $DEBUG > 2;
+ my $listref_or_error =
+ $tax_object->taxline( $totlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
+ unless (ref($listref_or_error)) {
+ $dbh->rollback if $oldAutoCommit;
+ return $listref_or_error;
+ }
+
+ warn "adding taxed tax amount ". $listref_or_error->[1].
+ " as ". $tax_object->taxname. "\n"
+ if $DEBUG;
+ $tax{ $tax } += $listref_or_error->[1];
+ }
+
+ #consolidate and create tax line items
+ warn "consolidating and generating...\n" if $DEBUG > 2;
+ foreach my $taxname ( keys %taxname ) {
+ my $tax = 0;
+ my %seen = ();
+ my @cust_bill_pkg_tax_location = ();
+ warn "adding $taxname\n" if $DEBUG > 1;
+ foreach my $taxitem ( @{ $taxname{$taxname} } ) {
+ next if $seen{$taxitem}++;
+ warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
+ $tax += $tax{$taxitem};
+ push @cust_bill_pkg_tax_location,
+ map { new FS::cust_bill_pkg_tax_location $_ }
+ @{ $tax_location{ $taxitem } };
+ }
+ next unless $tax;
+
+ $tax = sprintf('%.2f', $tax );
+ $total_setup = sprintf('%.2f', $total_setup+$tax );
+
+ push @cust_bill_pkg, new FS::cust_bill_pkg {
+ 'pkgnum' => 0,
+ 'setup' => $tax,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $taxname,
+ 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ };
+
+ }
+
+ my $charged = sprintf('%.2f', $total_setup + $total_recur );
+
+ #create the new invoice
+ my $cust_bill = new FS::cust_bill ( {
+ 'custnum' => $self->custnum,
+ '_date' => ( $invoice_time ),
+ 'charged' => $charged,
+ } );
+ my $error = $cust_bill->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't create invoice for customer #". $self->custnum. ": $error";
+ }
+
+ foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
+ $cust_bill_pkg->invnum($cust_bill->invnum);
+ my $error = $cust_bill_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't create invoice line item: $error";
+ }
+ }
+
+
+ foreach my $hook ( @precommit_hooks ) {
+ eval {
+ &{$hook}; #($self) ?
+ };
+ if ( $@ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$@ running precommit hook $hook\n";
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+}
+
+
+sub _make_lines {
+ my ($self, %params) = @_;
+
+ my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
+ my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
+ my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
+ my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
+ my $total_setup = $params{setup} or die "no setup accumulator specified";
+ my $total_recur = $params{recur} or die "no recur accumulator specified";
+ my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
+ my $time = $params{'time'} or die "no time specified";
+ my (%options) = %{$params{options}}; #hmmm only for 'resetup'
+
+ my $dbh = dbh;
+ my $real_pkgpart = $cust_pkg->pkgpart;
+ my %hash = $cust_pkg->hash;
+ my $old_cust_pkg = new FS::cust_pkg \%hash;
+
+ my @details = ();
+
+ my $lineitems = 0;
+
+ $cust_pkg->pkgpart($part_pkg->pkgpart);
+
+ ###
+ # bill setup
+ ###
+
+ my $setup = 0;
+ my $unitsetup = 0;
+ if ( ! $cust_pkg->setup &&
+ (
+ ( $conf->exists('disable_setup_suspended_pkgs') &&
+ ! $cust_pkg->getfield('susp')
+ ) || ! $conf->exists('disable_setup_suspended_pkgs')
+ )
+ || $options{'resetup'}
+ ) {
+
+ warn " bill setup\n" if $DEBUG > 1;
+ $lineitems++;
+
+ $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
+ return "$@ running calc_setup for $cust_pkg\n"
+ if $@;
+
+ $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+
+ $cust_pkg->setfield('setup', $time)
+ unless $cust_pkg->setup;
+ #do need it, but it won't get written to the db
+ #|| $cust_pkg->pkgpart != $real_pkgpart;
+
+ }
+
+ ###
+ # bill recurring fee
+ ###
+
+ #XXX unit stuff here too
+ my $recur = 0;
+ my $unitrecur = 0;
+ my $sdate;
+ if ( ! $cust_pkg->getfield('susp') and
+ ( $part_pkg->getfield('freq') ne '0' &&
+ ( $cust_pkg->getfield('bill') || 0 ) <= $time
+ )
+ || ( $part_pkg->plan eq 'voip_cdr'
+ && $part_pkg->option('bill_every_call')
+ )
+ ) {
+
+ # XXX should this be a package event? probably. events are called
+ # at collection time at the moment, though...
+ $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
+ if $part_pkg->can('reset_usage');
+ #don't want to reset usage just cause we want a line item??
+ #&& $part_pkg->pkgpart == $real_pkgpart;
+
+ warn " bill recur\n" if $DEBUG > 1;
+ $lineitems++;
+
+ # XXX shared with $recur_prog
+ $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+
+ #over two params! lets at least switch to a hashref for the rest...
+ my $increment_next_bill = ( $part_pkg->freq ne '0'
+ && ( $cust_pkg->getfield('bill') || 0 ) <= $time
+ );
+ my %param = ( 'precommit_hooks' => $precommit_hooks,
+ 'increment_next_bill' => $increment_next_bill,
+ );
+
+ $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
+ return "$@ running calc_recur for $cust_pkg\n"
+ if ( $@ );
+
+ if ( $increment_next_bill ) {
+
+ #change this bit to use Date::Manip? CAREFUL with timezones (see
+ # mailing list archive)
+ my ($sec,$min,$hour,$mday,$mon,$year) =
+ (localtime($sdate) )[0,1,2,3,4,5];
+
+ #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
+ # only for figuring next bill date, nothing else, so, reset $sdate again
+ # here
+ $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+ #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
+ $cust_pkg->last_bill($sdate);
+
+ if ( $part_pkg->freq =~ /^\d+$/ ) {
+ $mon += $part_pkg->freq;
+ until ( $mon < 12 ) { $mon -= 12; $year++; }
+ } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
+ my $weeks = $1;
+ $mday += $weeks * 7;
+ } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
+ my $days = $1;