1 package FS::cust_bill_pkg;
2 use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
5 use vars qw( @ISA $DEBUG $me );
7 use List::Util qw( sum min );
9 use FS::Record qw( qsearch qsearchs dbh );
12 use FS::cust_bill_pkg_detail;
13 use FS::cust_bill_pkg_display;
14 use FS::cust_bill_pkg_discount;
15 use FS::cust_bill_pkg_fee;
16 use FS::cust_bill_pay_pkg;
17 use FS::cust_credit_bill_pkg;
18 use FS::cust_tax_exempt_pkg;
19 use FS::cust_bill_pkg_tax_location;
20 use FS::cust_bill_pkg_tax_rate_location;
21 use FS::cust_tax_adjustment;
22 use FS::cust_bill_pkg_void;
23 use FS::cust_bill_pkg_detail_void;
24 use FS::cust_bill_pkg_display_void;
25 use FS::cust_bill_pkg_discount_void;
26 use FS::cust_bill_pkg_tax_location_void;
27 use FS::cust_bill_pkg_tax_rate_location_void;
28 use FS::cust_tax_exempt_pkg_void;
34 $me = '[FS::cust_bill_pkg]';
38 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
42 use FS::cust_bill_pkg;
44 $record = new FS::cust_bill_pkg \%hash;
45 $record = new FS::cust_bill_pkg { 'column' => 'value' };
47 $error = $record->insert;
49 $error = $record->check;
53 An FS::cust_bill_pkg object represents an invoice line item.
54 FS::cust_bill_pkg inherits from FS::Record. The following fields are
65 invoice (see L<FS::cust_bill>)
69 package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line)
71 =item pkgpart_override
73 optional package definition (see L<FS::part_pkg>) override
85 starting date of recurring fee
89 ending date of recurring fee
93 Line item description (overrides normal package description)
97 If not set, defaults to 1
101 If not set, defaults to setup
105 If not set, defaults to recur
109 If set to Y, indicates data should not appear as separate line item on invoice
113 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
114 see L<Time::Local> and L<Date::Parse> for conversion functions.
122 Creates a new line item. To add the line item to the database, see
123 L<"insert">. Line items are normally created by calling the bill method of a
124 customer object (see L<FS::cust_main>).
128 sub table { 'cust_bill_pkg'; }
130 sub detail_table { 'cust_bill_pkg_detail'; }
131 sub display_table { 'cust_bill_pkg_display'; }
132 sub discount_table { 'cust_bill_pkg_discount'; }
133 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
134 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
135 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
139 Adds this line item to the database. If there is an error, returns the error,
140 otherwise returns false.
147 local $SIG{HUP} = 'IGNORE';
148 local $SIG{INT} = 'IGNORE';
149 local $SIG{QUIT} = 'IGNORE';
150 local $SIG{TERM} = 'IGNORE';
151 local $SIG{TSTP} = 'IGNORE';
152 local $SIG{PIPE} = 'IGNORE';
154 my $oldAutoCommit = $FS::UID::AutoCommit;
155 local $FS::UID::AutoCommit = 0;
158 my $error = $self->SUPER::insert;
160 $dbh->rollback if $oldAutoCommit;
164 if ( $self->get('details') ) {
165 foreach my $detail ( @{$self->get('details')} ) {
166 $detail->billpkgnum($self->billpkgnum);
167 $error = $detail->insert;
169 $dbh->rollback if $oldAutoCommit;
170 return "error inserting cust_bill_pkg_detail: $error";
175 if ( $self->get('display') ) {
176 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
177 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
178 $error = $cust_bill_pkg_display->insert;
180 $dbh->rollback if $oldAutoCommit;
181 return "error inserting cust_bill_pkg_display: $error";
186 if ( $self->get('discounts') ) {
187 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
188 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
189 $error = $cust_bill_pkg_discount->insert;
191 $dbh->rollback if $oldAutoCommit;
192 return "error inserting cust_bill_pkg_discount: $error";
197 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
198 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
199 $error = $cust_tax_exempt_pkg->insert;
201 $dbh->rollback if $oldAutoCommit;
202 return "error inserting cust_tax_exempt_pkg: $error";
206 my $tax_location = $self->get('cust_bill_pkg_tax_location');
207 if ( $tax_location ) {
208 foreach my $link ( @$tax_location ) {
209 next if $link->billpkgtaxlocationnum; # don't try to double-insert
210 # This cust_bill_pkg can be linked on either side (i.e. it can be the
211 # tax or the taxed item). If the other side is already inserted,
212 # then set billpkgnum to ours, and insert the link. Otherwise,
213 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
214 # on the other side, to be inserted later.
216 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
217 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
218 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
219 # break circular links when doing this
220 $link->set('tax_cust_bill_pkg', '');
222 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
223 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
224 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
225 # XXX if we ever do tax-on-tax for these, this will have to change
226 # since pkgnum will be zero
227 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
228 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
229 $link->set('taxable_cust_bill_pkg', '');
232 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
233 $error = $link->insert;
235 $dbh->rollback if $oldAutoCommit;
236 return "error inserting cust_bill_pkg_tax_location: $error";
240 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
241 : $link->get('tax_cust_bill_pkg');
242 my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
243 push @$link_array, $link;
244 $other->set('cust_bill_pkg_tax_location' => $link_array);
249 # someday you will be as awesome as cust_bill_pkg_tax_location...
251 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
252 if ( $tax_rate_location ) {
253 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
254 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
255 $error = $cust_bill_pkg_tax_rate_location->insert;
257 $dbh->rollback if $oldAutoCommit;
258 return "error inserting cust_bill_pkg_tax_rate_location: $error";
263 my $fee_links = $self->get('cust_bill_pkg_fee');
265 foreach my $link ( @$fee_links ) {
266 # very similar to cust_bill_pkg_tax_location, for obvious reasons
267 next if $link->billpkgfeenum; # don't try to double-insert
269 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
270 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
272 if ( $target and $target->billpkgnum ) {
273 $link->set('billpkgnum', $target->billpkgnum);
274 # base_invnum => null indicates that the fee is based on its own
276 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
277 $link->set('cust_bill_pkg', '');
280 if ( $base and $base->billpkgnum ) {
281 $link->set('base_billpkgnum', $base->billpkgnum);
282 $link->set('base_cust_bill_pkg', '');
284 # it's based on a line item that's not yet inserted
285 my $link_array = $base->get('cust_bill_pkg_fee') || [];
286 push @$link_array, $link;
287 $base->set('cust_bill_pkg_fee' => $link_array);
288 next; # don't insert the link yet
291 $error = $link->insert;
293 $dbh->rollback if $oldAutoCommit;
294 return "error inserting cust_bill_pkg_fee: $error";
299 my $cust_event_fee = $self->get('cust_event_fee');
300 if ( $cust_event_fee ) {
301 $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
302 $error = $cust_event_fee->replace;
304 $dbh->rollback if $oldAutoCommit;
305 return "error updating cust_event_fee: $error";
309 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
310 if ( $cust_tax_adjustment ) {
311 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
312 $error = $cust_tax_adjustment->replace;
314 $dbh->rollback if $oldAutoCommit;
315 return "error replacing cust_tax_adjustment: $error";
319 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
326 Voids this line item: deletes the line item and adds a record of the voided
327 line item to the FS::cust_bill_pkg_void table (and related tables).
333 my $reason = scalar(@_) ? shift : '';
335 local $SIG{HUP} = 'IGNORE';
336 local $SIG{INT} = 'IGNORE';
337 local $SIG{QUIT} = 'IGNORE';
338 local $SIG{TERM} = 'IGNORE';
339 local $SIG{TSTP} = 'IGNORE';
340 local $SIG{PIPE} = 'IGNORE';
342 my $oldAutoCommit = $FS::UID::AutoCommit;
343 local $FS::UID::AutoCommit = 0;
346 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
347 map { $_ => $self->get($_) } $self->fields
349 $cust_bill_pkg_void->reason($reason);
350 my $error = $cust_bill_pkg_void->insert;
352 $dbh->rollback if $oldAutoCommit;
356 foreach my $table (qw(
358 cust_bill_pkg_display
359 cust_bill_pkg_discount
360 cust_bill_pkg_tax_location
361 cust_bill_pkg_tax_rate_location
365 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
367 my $vclass = 'FS::'.$table.'_void';
368 my $void = $vclass->new( {
369 map { $_ => $linked->get($_) } $linked->fields
371 my $error = $void->insert || $linked->delete;
373 $dbh->rollback if $oldAutoCommit;
381 $error = $self->delete;
383 $dbh->rollback if $oldAutoCommit;
387 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
402 local $SIG{HUP} = 'IGNORE';
403 local $SIG{INT} = 'IGNORE';
404 local $SIG{QUIT} = 'IGNORE';
405 local $SIG{TERM} = 'IGNORE';
406 local $SIG{TSTP} = 'IGNORE';
407 local $SIG{PIPE} = 'IGNORE';
409 my $oldAutoCommit = $FS::UID::AutoCommit;
410 local $FS::UID::AutoCommit = 0;
413 foreach my $table (qw(
415 cust_bill_pkg_display
416 cust_bill_pkg_discount
417 cust_bill_pkg_tax_location
418 cust_bill_pkg_tax_rate_location
424 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
425 my $error = $linked->delete;
427 $dbh->rollback if $oldAutoCommit;
434 foreach my $cust_tax_adjustment (
435 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
437 $cust_tax_adjustment->billpkgnum(''); #NULL
438 my $error = $cust_tax_adjustment->replace;
440 $dbh->rollback if $oldAutoCommit;
445 my $error = $self->SUPER::delete(@_);
447 $dbh->rollback if $oldAutoCommit;
451 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
457 #alas, bin/follow-tax-rename
459 #=item replace OLD_RECORD
461 #Currently unimplemented. This would be even more of an accounting nightmare
462 #than deleteing the items. Just don't do it.
467 # return "Can't modify cust_bill_pkg records!";
472 Checks all fields to make sure this is a valid line item. If there is an
473 error, returns the error, otherwise returns false. Called by the insert
482 $self->ut_numbern('billpkgnum')
483 || $self->ut_snumber('pkgnum')
484 || $self->ut_number('invnum')
485 || $self->ut_money('setup')
486 || $self->ut_money('recur')
487 || $self->ut_numbern('sdate')
488 || $self->ut_numbern('edate')
489 || $self->ut_textn('itemdesc')
490 || $self->ut_textn('itemcomment')
491 || $self->ut_enum('hidden', [ '', 'Y' ])
493 return $error if $error;
495 $self->regularize_details;
497 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
498 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
499 return "Unknown pkgnum ". $self->pkgnum
500 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
503 return "Unknown invnum"
504 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
509 =item regularize_details
511 Converts the contents of the 'details' pseudo-field to
512 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
516 sub regularize_details {
518 if ( $self->get('details') ) {
519 foreach my $detail ( @{$self->get('details')} ) {
520 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
521 # then turn it into one
523 if ( ! ref($detail) ) {
524 $hash{'detail'} = $detail;
526 elsif ( ref($detail) eq 'HASH' ) {
529 elsif ( ref($detail) eq 'ARRAY' ) {
530 carp "passing invoice details as arrays is deprecated";
531 #carp "this way sucks, use a hash"; #but more useful/friendly
532 $hash{'format'} = $detail->[0];
533 $hash{'detail'} = $detail->[1];
534 $hash{'amount'} = $detail->[2];
535 $hash{'classnum'} = $detail->[3];
536 $hash{'phonenum'} = $detail->[4];
537 $hash{'accountcode'} = $detail->[5];
538 $hash{'startdate'} = $detail->[6];
539 $hash{'duration'} = $detail->[7];
540 $hash{'regionname'} = $detail->[8];
543 die "unknown detail type ". ref($detail);
545 $detail = new FS::cust_bill_pkg_detail \%hash;
547 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
555 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
561 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
566 Returns the customer (L<FS::cust_main> object) for this line item.
571 # required for cust_main_Mixin equivalence
572 # and use cust_bill instead of cust_pkg because this might not have a
575 my $cust_bill = $self->cust_bill or return '';
576 $cust_bill->cust_main;
579 =item previous_cust_bill_pkg
581 Returns the previous cust_bill_pkg for this package, if any.
585 sub previous_cust_bill_pkg {
587 return unless $self->sdate;
589 'table' => 'cust_bill_pkg',
590 'hashref' => { 'pkgnum' => $self->pkgnum,
591 'sdate' => { op=>'<', value=>$self->sdate },
593 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
599 Returns the amount owed (still outstanding) on this line item's setup fee,
600 which is the amount of the line item minus all payment applications (see
601 L<FS::cust_bill_pay_pkg> and credit applications (see
602 L<FS::cust_credit_bill_pkg>).
608 $self->owed('setup', @_);
613 Returns the amount owed (still outstanding) on this line item's recurring fee,
614 which is the amount of the line item minus all payment applications (see
615 L<FS::cust_bill_pay_pkg> and credit applications (see
616 L<FS::cust_credit_bill_pkg>).
622 $self->owed('recur', @_);
625 # modeled after cust_bill::owed...
627 my( $self, $field ) = @_;
628 my $balance = $self->$field();
629 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
630 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
631 $balance = sprintf( '%.2f', $balance );
632 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
638 my( $self, $field ) = @_;
639 my $balance = $self->$field();
640 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
641 $balance = sprintf( '%.2f', $balance );
642 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
646 sub cust_bill_pay_pkg {
647 my( $self, $field ) = @_;
648 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
649 'setuprecur' => $field,
654 sub cust_credit_bill_pkg {
655 my( $self, $field ) = @_;
656 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
657 'setuprecur' => $field,
664 Returns the number of billing units (for tax purposes) represented by this,
671 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
675 =item set_display OPTION => VALUE ...
677 A helper method for I<insert>, populates the pseudo-field B<display> with
678 appropriate FS::cust_bill_pkg_display objects.
680 Options are passed as a list of name/value pairs. Options are:
682 part_pkg: FS::part_pkg object from this line item's package.
684 real_pkgpart: if this line item comes from a bundled package, the pkgpart
685 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
690 my( $self, %opt ) = @_;
691 my $part_pkg = $opt{'part_pkg'};
692 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
694 my $conf = new FS::Conf;
696 # whether to break this down into setup/recur/usage
697 my $separate = $conf->exists('separate_usage');
699 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
700 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
702 # or use the category from $opt{'part_pkg'} if its not bundled?
703 my $categoryname = $cust_pkg->part_pkg->categoryname;
705 # if we don't have to separate setup/recur/usage, or put this in a
706 # package-specific section, or display a usage summary, then don't
707 # even create one of these. The item will just display in the unnamed
708 # section as a single line plus details.
709 return $self->set('display', [])
710 unless $separate || $categoryname || $usage_mandate;
714 my %hash = ( 'section' => $categoryname );
716 # whether to put usage details in a separate section, and if so, which one
717 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
718 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
720 # whether to show a usage summary line (total usage charges, no details)
721 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
722 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
725 # create lines for setup and (non-usage) recur, in the main section
726 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
727 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
729 # display everything in a single line
730 push @display, new FS::cust_bill_pkg_display
733 # and if usage_mandate is enabled, hide details
734 # (this only works on multisection invoices...)
735 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
739 if ($separate && $usage_section && $summary) {
740 # create a line for the usage summary in the main section
741 push @display, new FS::cust_bill_pkg_display { type => 'U',
747 if ($usage_mandate || ($usage_section && $summary) ) {
748 $hash{post_total} = 'Y';
751 if ($separate || $usage_mandate) {
752 # show call details for this line item in the usage section.
753 # if usage_mandate is on, this will display below the section subtotal.
754 # this also happens if usage is in a separate section and there's a
755 # summary in the main section, though I'm not sure why.
756 $hash{section} = $usage_section if $usage_section;
757 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
760 $self->set('display', \@display);
766 Returns a hash: keys are "setup", "recur" or usage classnum, values are
767 FS::cust_bill_pkg objects, each with no more than a single class (setup or
774 # XXX this goes away with cust_bill_pkg refactor
776 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
777 my %cust_bill_pkg = ();
779 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
780 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
783 #split setup and recur
784 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
785 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
786 $cust_bill_pkg->set('details', []);
787 $cust_bill_pkg->recur(0);
788 $cust_bill_pkg->unitrecur(0);
789 $cust_bill_pkg->type('');
790 $cust_bill_pkg_recur->setup(0);
791 $cust_bill_pkg_recur->unitsetup(0);
792 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
796 #split usage from recur
797 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
798 if exists($cust_bill_pkg{recur});
799 warn "usage is $usage\n" if $DEBUG > 1;
801 my $cust_bill_pkg_usage =
802 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
803 $cust_bill_pkg_usage->recur( $usage );
804 $cust_bill_pkg_usage->type( 'U' );
805 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
806 $cust_bill_pkg{recur}->recur( $recur );
807 $cust_bill_pkg{recur}->type( '' );
808 $cust_bill_pkg{recur}->set('details', []);
809 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
812 #subdivide usage by usage_class
813 if (exists($cust_bill_pkg{''})) {
814 foreach my $class (grep { $_ } $self->usage_classes) {
815 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
816 my $cust_bill_pkg_usage =
817 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
818 $cust_bill_pkg_usage->recur( $usage );
819 $cust_bill_pkg_usage->set('details', []);
820 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
821 $cust_bill_pkg{''}->recur( $classless );
822 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
824 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
825 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
826 delete $cust_bill_pkg{''}
827 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
830 # # sort setup,recur,'', and the rest numeric && return
831 # my @result = map { $cust_bill_pkg{$_} }
832 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
833 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
835 # keys %cust_bill_pkg;
844 Returns the amount of the charge associated with usage class CLASSNUM if
845 CLASSNUM is defined. Otherwise returns the total charge associated with
851 my( $self, $classnum ) = @_;
852 $self->regularize_details;
854 if ( $self->get('details') ) {
857 map { $_->amount || 0 }
858 grep { !defined($classnum) or $classnum eq $_->classnum }
859 @{ $self->get('details') }
864 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
865 ' WHERE billpkgnum = '. $self->billpkgnum;
866 $sql .= " AND classnum = $classnum" if defined($classnum);
868 my $sth = dbh->prepare($sql) or die dbh->errstr;
869 $sth->execute or die $sth->errstr;
871 return $sth->fetchrow_arrayref->[0] || 0;
879 Returns a list of usage classnums associated with this invoice line's
886 $self->regularize_details;
888 if ( $self->get('details') ) {
890 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
896 qsearch({ table => 'cust_bill_pkg_detail',
897 hashref => { billpkgnum => $self->billpkgnum },
898 select => 'DISTINCT classnum',
905 sub cust_tax_exempt_pkg {
908 $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
911 =item cust_bill_pkg_fee
913 Returns the list of associated cust_bill_pkg_fee objects, if this is
918 sub cust_bill_pkg_fee {
920 qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
923 =item cust_bill_pkg_tax_Xlocation
925 Returns the list of associated cust_bill_pkg_tax_location and/or
926 cust_bill_pkg_tax_rate_location objects
930 sub cust_bill_pkg_tax_Xlocation {
933 my %hash = ( 'billpkgnum' => $self->billpkgnum );
936 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
937 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
942 =item recur_show_zero
946 sub recur_show_zero { shift->_X_show_zero('recur'); }
947 sub setup_show_zero { shift->_X_show_zero('setup'); }
950 my( $self, $what ) = @_;
952 return 0 unless $self->$what() == 0 && $self->pkgnum;
954 $self->cust_pkg->_X_show_zero($what);
957 =item credited [ BEFORE, AFTER, OPTIONS ]
959 Returns the sum of credits applied to this item. Arguments are the same as
960 owed_sql/paid_sql/credited_sql.
966 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
969 =item tax_locationnum
971 Returns the L<FS::cust_location> number that this line item is in for tax
972 purposes. For package sales, it's the package tax location; for fees,
973 it's the customer's default service location.
977 sub tax_locationnum {
979 if ( $self->pkgnum ) { # normal sales
980 return $self->cust_pkg->tax_locationnum;
981 } elsif ( $self->feepart ) { # fees
982 return $self->cust_bill->cust_main->ship_locationnum;
990 if ( $self->pkgnum ) { # normal sales
991 return $self->cust_pkg->tax_location;
992 } elsif ( $self->feepart ) { # fees
993 return $self->cust_bill->cust_main->ship_location;
1001 Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
1002 charge. If called on a tax line, returns nothing.
1008 if ( $self->pkgpart_override ) {
1009 return FS::part_pkg->by_key($self->pkgpart_override);
1010 } elsif ( $self->pkgnum ) {
1011 return $self->cust_pkg->part_pkg;
1012 } elsif ( $self->feepart ) {
1013 return $self->part_fee;
1024 ? FS::part_fee->by_key($self->feepart)
1030 =head1 CLASS METHODS
1036 Returns an SQL expression for the total usage charges in details on
1042 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1043 FROM cust_bill_pkg_detail
1044 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1046 sub usage_sql { $usage_sql }
1048 # this makes owed_sql, etc. much more concise
1050 my ($class, $start, $end, %opt) = @_;
1051 my $setuprecur = $opt{setuprecur} || '';
1053 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1054 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1055 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1057 if ($opt{no_usage} and $charged =~ /recur/) {
1058 $charged = "$charged - $usage_sql"
1065 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1067 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1068 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1069 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1075 '(' . $class->charged_sql(@_) .
1076 ' - ' . $class->paid_sql(@_) .
1077 ' - ' . $class->credited_sql(@_) . ')'
1080 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1082 Returns an SQL expression for the sum of payments applied to this item.
1087 my ($class, $start, $end, %opt) = @_;
1088 my $s = $start ? "AND cust_pay._date <= $start" : '';
1089 my $e = $end ? "AND cust_pay._date > $end" : '';
1090 my $setuprecur = $opt{setuprecur} || '';
1091 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1092 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1093 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1095 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1096 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1097 JOIN cust_pay USING (paynum)
1098 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1099 $s $e $setuprecur )";
1101 if ( $opt{no_usage} ) {
1102 # cap the amount paid at the sum of non-usage charges,
1103 # minus the amount credited against non-usage charges
1105 $class->charged_sql($start, $end, %opt) . ' - ' .
1106 $class->credited_sql($start, $end, %opt).')';
1115 my ($class, $start, $end, %opt) = @_;
1116 my $s = $start ? "AND cust_credit._date <= $start" : '';
1117 my $e = $end ? "AND cust_credit._date > $end" : '';
1118 my $setuprecur = $opt{setuprecur} || '';
1119 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1120 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1121 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1123 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1124 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1125 JOIN cust_credit USING (crednum)
1126 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1127 $s $e $setuprecur )";
1129 if ( $opt{no_usage} ) {
1130 # cap the amount credited at the sum of non-usage charges
1131 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1139 sub upgrade_tax_location {
1140 # For taxes that were calculated/invoiced before cust_location refactoring
1141 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1142 # they were calculated on a package-location basis. Create them here,
1143 # along with any necessary cust_location records and any tax exemption
1146 my ($class, %opt) = @_;
1147 # %opt may include 's' and 'e': start and end date ranges
1148 # and 'X': abort on any error, instead of just rolling back changes to
1151 my $oldAutoCommit = $FS::UID::AutoCommit;
1152 local $FS::UID::AutoCommit = 0;
1155 use FS::h_cust_main;
1156 use FS::h_cust_bill;
1158 use FS::h_cust_main_exemption;
1161 local $FS::cust_location::import = 1;
1163 my $conf = FS::Conf->new; # h_conf?
1164 return if $conf->exists('enable_taxproducts'); #don't touch this case
1165 my $use_ship = $conf->exists('tax-ship_address');
1166 my $use_pkgloc = $conf->exists('tax-pkg_address');
1168 my $date_where = '';
1170 $date_where .= " AND cust_bill._date >= $opt{s}";
1173 $date_where .= " AND cust_bill._date < $opt{e}";
1176 my $commit_each_invoice = 1 unless $opt{X};
1178 # if an invoice has either of these kinds of objects, then it doesn't
1179 # need to be upgraded...probably
1180 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1181 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1182 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1183 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1184 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1185 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1186 ' AND exempt_monthly IS NULL';
1188 my %all_tax_names = (
1191 map { $_->taxname => 1 }
1192 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1195 my $search = FS::Cursor->new({
1196 table => 'cust_bill',
1198 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1199 "AND NOT EXISTS($sub_has_exempt) ".
1203 #print "Processing ".scalar(@invnums)." invoices...\n";
1207 while (my $cust_bill = $search->fetch) {
1208 my $invnum = $cust_bill->invnum;
1210 print STDERR "Invoice #$invnum\n";
1212 my %pkgpart_taxclass; # pkgpart => taxclass
1213 my %pkgpart_exempt_setup;
1214 my %pkgpart_exempt_recur;
1215 my $h_cust_bill = qsearchs('h_cust_bill',
1216 { invnum => $invnum,
1217 history_action => 'insert' });
1218 if (!$h_cust_bill) {
1219 warn "no insert record for invoice $invnum; skipped\n";
1220 #$date = $cust_bill->_date as a fallback?
1221 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1222 # when looking up history records in other tables.
1225 my $custnum = $h_cust_bill->custnum;
1227 # Determine the address corresponding to this tax region.
1228 # It's either the bill or ship address of the customer as of the
1229 # invoice date-of-insertion. (Not necessarily the invoice date.)
1230 my $date = $h_cust_bill->history_date;
1231 my $h_cust_main = qsearchs('h_cust_main',
1232 { custnum => $custnum },
1233 FS::h_cust_main->sql_h_searchs($date)
1235 if (!$h_cust_main ) {
1236 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1238 # fallback to current $cust_main? sounds dangerous.
1241 # This is a historical customer record, so it has a historical address.
1242 # If there's no cust_location matching this custnum and address (there
1243 # probably isn't), create one.
1244 my %tax_loc; # keys are pkgnums, values are cust_location objects
1245 my $default_tax_loc;
1246 if ( $h_cust_main->bill_locationnum ) {
1247 # the location has already been upgraded
1249 $default_tax_loc = $h_cust_main->ship_location;
1251 $default_tax_loc = $h_cust_main->bill_location;
1254 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1255 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1256 FS::cust_main->location_fields;
1257 # not really needed for this, and often result in duplicate locations
1258 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1260 $hash{custnum} = $h_cust_main->custnum;
1261 $default_tax_loc = FS::cust_location->new(\%hash);
1262 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1264 warn "couldn't create historical location record for cust#".
1265 $h_cust_main->custnum.": $error\n";
1270 $exempt_cust = 1 if $h_cust_main->tax;
1272 # classify line items
1274 my %nontax_items; # taxclass => array of cust_bill_pkg
1275 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1276 my $pkgnum = $item->pkgnum;
1278 if ( $pkgnum == 0 ) {
1280 push @tax_items, $item;
1283 # (pkgparts really shouldn't change, right?)
1284 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1285 FS::h_cust_pkg->sql_h_searchs($date)
1287 if ( !$h_cust_pkg ) {
1288 warn "no historical package #".$item->pkgpart."; skipped\n";
1291 my $pkgpart = $h_cust_pkg->pkgpart;
1293 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1294 # then this package already had a locationnum assigned, and that's
1295 # the one to use for tax calculation
1296 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1298 # use the customer's bill or ship loc, which was inserted earlier
1299 $tax_loc{$pkgnum} = $default_tax_loc;
1302 if (!exists $pkgpart_taxclass{$pkgpart}) {
1303 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1304 FS::h_part_pkg->sql_h_searchs($date)
1306 if ( !$h_part_pkg ) {
1307 warn "no historical package def #$pkgpart; skipped\n";
1310 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1311 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1312 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1315 # mark any exemptions that apply
1316 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1317 $item->set('exempt_setup' => 1);
1320 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1321 $item->set('exempt_recur' => 1);
1324 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1326 $nontax_items{$taxclass} ||= [];
1327 push @{ $nontax_items{$taxclass} }, $item;
1331 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1334 # Get any per-customer taxname exemptions that were in effect.
1335 my %exempt_cust_taxname;
1336 foreach (keys %all_tax_names) {
1337 my $h_exemption = qsearchs('h_cust_main_exemption', {
1338 'custnum' => $custnum,
1341 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1344 $exempt_cust_taxname{ $_ } = 1;
1348 # Use a variation on the procedure in
1349 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1351 my @loc_keys = qw( district city county state country );
1352 my %taxdef_by_name; # by name, and then by taxclass
1353 my %est_tax; # by name, and then by taxclass
1354 my %taxable_items; # by taxnum, and then an array
1356 foreach my $taxclass (keys %nontax_items) {
1357 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1358 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1359 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1360 my @elim = qw( district city county state );
1361 my @taxdefs; # because there may be several with different taxnames
1363 $myhash{taxclass} = $taxclass;
1364 @taxdefs = qsearch('cust_main_county', \%myhash);
1366 $myhash{taxclass} = '';
1367 @taxdefs = qsearch('cust_main_county', \%myhash);
1369 $myhash{ shift @elim } = '';
1370 } while scalar(@elim) and !@taxdefs;
1372 foreach my $taxdef (@taxdefs) {
1373 next if $taxdef->tax == 0;
1374 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1376 $taxable_items{$taxdef->taxnum} ||= [];
1377 # clone the item so that taxdef-dependent changes don't
1378 # change it for other taxdefs
1379 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1381 # these flags are already set if the part_pkg declares itself exempt
1382 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1383 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1386 my $taxable = $item->setup + $item->recur;
1388 # h_cust_credit_bill_pkg?
1389 # NO. Because if these exemptions HAD been created at the time of
1390 # billing, and then a credit applied later, the exemption would
1391 # have been adjusted by the amount of the credit. So we adjust
1392 # the taxable amount before creating the exemption.
1393 # But don't deduct the credit from taxable, because the tax was
1394 # calculated before the credit was applied.
1395 foreach my $f (qw(setup recur)) {
1396 my $credited = FS::Record->scalar_sql(
1397 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1398 "WHERE billpkgnum = ? AND setuprecur = ?",
1402 $item->set($f, $item->get($f) - $credited) if $credited;
1404 my $existing_exempt = FS::Record->scalar_sql(
1405 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1406 "billpkgnum = ? AND taxnum = ?",
1407 $item->billpkgnum, $taxdef->taxnum
1409 $taxable -= $existing_exempt;
1411 if ( $taxable and $exempt_cust ) {
1412 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1415 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1416 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1419 if ( $taxable and $item->exempt_setup ) {
1420 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1421 $taxable -= $item->setup;
1423 if ( $taxable and $item->exempt_recur ) {
1424 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1425 $taxable -= $item->recur;
1428 $item->set('taxable' => $taxable);
1429 push @{ $taxable_items{$taxdef->taxnum} }, $item
1432 # estimate the amount of tax (this is necessary because different
1433 # taxdefs with the same taxname may have different tax rates)
1434 # and sum that for each taxname/taxclass combination
1436 $est_tax{$taxdef->taxname} ||= {};
1437 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1438 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1439 $taxable * $taxdef->tax;
1441 foreach (@new_exempt) {
1442 next if $_->{amount} == 0;
1443 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1445 billpkgnum => $item->billpkgnum,
1446 taxnum => $taxdef->taxnum,
1448 my $error = $cust_tax_exempt_pkg->insert;
1450 my $pkgnum = $item->pkgnum;
1451 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1455 } #foreach @new_exempt
1458 } #foreach $taxclass
1460 # Now go through the billed taxes and match them up with the line items.
1461 TAX_ITEM: foreach my $tax_item ( @tax_items )
1463 my $taxname = $tax_item->itemdesc;
1464 $taxname = '' if $taxname eq 'Tax';
1466 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1467 # then we didn't find any applicable taxes with this name
1468 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1469 # possibly all of these should be "next TAX_ITEM", but whole invoices
1470 # are transaction protected and we can go back and retry them.
1473 # classname => cust_main_county
1474 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1476 # Divide the tax item among taxclasses, if necessary
1477 # classname => estimated tax amount
1478 my $this_est_tax = $est_tax{$taxname};
1479 if (!defined $this_est_tax) {
1480 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1483 my $est_total = sum(values %$this_est_tax);
1484 if ( $est_total == 0 ) {
1486 warn "estimated tax on invoice #$invnum is zero.\n";
1490 my $real_tax = $tax_item->setup;
1491 printf ("Distributing \$%.2f tax:\n", $real_tax);
1492 my $cents_remaining = $real_tax * 100; # for rounding error
1493 my @tax_links; # partial CBPTL hashrefs
1494 foreach my $taxclass (keys %taxdef_by_class) {
1495 my $taxdef = $taxdef_by_class{$taxclass};
1496 # these items already have "taxable" set to their charge amount
1497 # after applying any credits or exemptions
1498 my @items = @{ $taxable_items{$taxdef->taxnum} };
1499 my $subtotal = sum(map {$_->get('taxable')} @items);
1500 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1502 foreach my $nontax (@items) {
1503 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1504 my $part = int($real_tax
1506 * ($this_est_tax->{$taxclass}/$est_total)
1508 * ($nontax->get('taxable'))/$subtotal
1512 $cents_remaining -= $part;
1514 taxnum => $taxdef->taxnum,
1515 pkgnum => $nontax->pkgnum,
1516 locationnum => $my_tax_loc->locationnum,
1517 billpkgnum => $nontax->billpkgnum,
1521 } #foreach $taxclass
1522 # Distribute any leftover tax round-robin style, one cent at a time.
1524 my $nlinks = scalar(@tax_links);
1526 # ensure that it really is an integer
1527 $cents_remaining = sprintf('%.0f', $cents_remaining);
1528 while ($cents_remaining > 0) {
1529 $tax_links[$i % $nlinks]->{cents} += 1;
1534 warn "Can't create tax links--no taxable items found.\n";
1538 # Gather credit/payment applications so that we can link them
1541 qsearch( 'cust_credit_bill_pkg',
1542 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1544 qsearch( 'cust_bill_pay_pkg',
1545 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1549 # grab the first one
1550 my $this_unlinked = shift @unlinked;
1551 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1553 # Create tax links (yay!)
1554 printf("Creating %d tax links.\n",scalar(@tax_links));
1555 foreach (@tax_links) {
1556 my $link = FS::cust_bill_pkg_tax_location->new({
1557 billpkgnum => $tax_item->billpkgnum,
1558 taxtype => 'FS::cust_main_county',
1559 locationnum => $_->{locationnum},
1560 taxnum => $_->{taxnum},
1561 pkgnum => $_->{pkgnum},
1562 amount => sprintf('%.2f', $_->{cents} / 100),
1563 taxable_billpkgnum => $_->{billpkgnum},
1565 my $error = $link->insert;
1567 warn "Can't create tax link for inv#$invnum: $error\n";
1571 my $link_cents = $_->{cents};
1572 # update/create subitem links
1574 # If $this_unlinked is undef, then we've allocated all of the
1575 # credit/payment applications to the tax item. If $link_cents is 0,
1576 # then we've applied credits/payments to all of this package fraction,
1577 # so go on to the next.
1578 while ($this_unlinked and $link_cents) {
1579 # apply as much as possible of $link_amount to this credit/payment
1581 my $apply_cents = min($link_cents, $unlinked_cents);
1582 $link_cents -= $apply_cents;
1583 $unlinked_cents -= $apply_cents;
1584 # $link_cents or $unlinked_cents or both are now zero
1585 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1586 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1587 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1588 if ( $this_unlinked->$pkey ) {
1589 # then it's an existing link--replace it
1590 $error = $this_unlinked->replace;
1592 $this_unlinked->insert;
1594 # what do we do with errors at this stage?
1596 warn "Error creating tax application link: $error\n";
1597 next INVOICE; # for lack of a better idea
1600 if ( $unlinked_cents == 0 ) {
1601 # then we've allocated all of this payment/credit application,
1602 # so grab the next one
1603 $this_unlinked = shift @unlinked;
1604 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1605 } elsif ( $link_cents == 0 ) {
1606 # then we've covered all of this package tax fraction, so split
1607 # off a new application from this one
1608 $this_unlinked = $this_unlinked->new({
1609 $this_unlinked->hash,
1612 # $unlinked_cents is still what it is
1615 } #while $this_unlinked and $link_cents
1616 } #foreach (@tax_links)
1617 } #foreach $tax_item
1619 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1625 $dbh->rollback if $oldAutoCommit;
1626 die "Upgrade halted.\n" unless $commit_each_invoice;
1630 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1635 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1639 use Date::Parse 'str2time';
1642 my $upgrade = 'tax_location_2012';
1643 return if FS::upgrade_journal->is_done($upgrade);
1644 my $job = FS::queue->new({
1645 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1647 # call it kind of like a class method, not that it matters much
1648 $job->insert($class, 's' => str2time('2012-01-01'));
1649 # if there's a customer location upgrade queued also, wait for it to
1651 my $location_job = qsearchs('queue', {
1652 job => 'FS::cust_main::Location::process_upgrade_location'
1654 if ( $location_job ) {
1655 $job->depend_insert($location_job->jobnum);
1657 # Then mark the upgrade as done, so that we don't queue the job twice
1658 # and somehow run two of them concurrently.
1659 FS::upgrade_journal->set_done($upgrade);
1660 # This upgrade now does the job of assigning taxable_billpkgnums to
1661 # cust_bill_pkg_tax_location, so set that task done also.
1662 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1669 setup and recur shouldn't be separate fields. There should be one "amount"
1670 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1672 A line item with both should really be two separate records (preserving
1673 sdate and edate for setup fees for recurring packages - that information may
1674 be valuable later). Invoice generation (cust_main::bill), invoice printing
1675 (cust_bill), tax reports (report_tax.cgi) and line item reports
1676 (cust_bill_pkg.cgi) would need to be updated.
1678 owed_setup and owed_recur could then be repaced by just owed, and
1679 cust_bill::open_cust_bill_pkg and
1680 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1682 The upgrade procedure is pretty sketchy.
1686 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1687 from the base documentation.