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;
32 $me = '[FS::cust_bill_pkg]';
36 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
40 use FS::cust_bill_pkg;
42 $record = new FS::cust_bill_pkg \%hash;
43 $record = new FS::cust_bill_pkg { 'column' => 'value' };
45 $error = $record->insert;
47 $error = $record->check;
51 An FS::cust_bill_pkg object represents an invoice line item.
52 FS::cust_bill_pkg inherits from FS::Record. The following fields are
63 invoice (see L<FS::cust_bill>)
67 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)
69 =item pkgpart_override
71 optional package definition (see L<FS::part_pkg>) override
83 starting date of recurring fee
87 ending date of recurring fee
91 Line item description (overrides normal package description)
95 If not set, defaults to 1
99 If not set, defaults to setup
103 If not set, defaults to recur
107 If set to Y, indicates data should not appear as separate line item on invoice
111 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
112 see L<Time::Local> and L<Date::Parse> for conversion functions.
120 Creates a new line item. To add the line item to the database, see
121 L<"insert">. Line items are normally created by calling the bill method of a
122 customer object (see L<FS::cust_main>).
126 sub table { 'cust_bill_pkg'; }
128 sub detail_table { 'cust_bill_pkg_detail'; }
129 sub display_table { 'cust_bill_pkg_display'; }
130 sub discount_table { 'cust_bill_pkg_discount'; }
131 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
132 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
133 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
137 Adds this line item to the database. If there is an error, returns the error,
138 otherwise returns false.
145 local $SIG{HUP} = 'IGNORE';
146 local $SIG{INT} = 'IGNORE';
147 local $SIG{QUIT} = 'IGNORE';
148 local $SIG{TERM} = 'IGNORE';
149 local $SIG{TSTP} = 'IGNORE';
150 local $SIG{PIPE} = 'IGNORE';
152 my $oldAutoCommit = $FS::UID::AutoCommit;
153 local $FS::UID::AutoCommit = 0;
156 my $error = $self->SUPER::insert;
158 $dbh->rollback if $oldAutoCommit;
162 if ( $self->get('details') ) {
163 foreach my $detail ( @{$self->get('details')} ) {
164 $detail->billpkgnum($self->billpkgnum);
165 $error = $detail->insert;
167 $dbh->rollback if $oldAutoCommit;
168 return "error inserting cust_bill_pkg_detail: $error";
173 if ( $self->get('display') ) {
174 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
175 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
176 $error = $cust_bill_pkg_display->insert;
178 $dbh->rollback if $oldAutoCommit;
179 return "error inserting cust_bill_pkg_display: $error";
184 if ( $self->get('discounts') ) {
185 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
186 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
187 $error = $cust_bill_pkg_discount->insert;
189 $dbh->rollback if $oldAutoCommit;
190 return "error inserting cust_bill_pkg_discount: $error";
195 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
196 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
197 $error = $cust_tax_exempt_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "error inserting cust_tax_exempt_pkg: $error";
204 my $tax_location = $self->get('cust_bill_pkg_tax_location');
205 if ( $tax_location ) {
206 foreach my $link ( @$tax_location ) {
207 next if $link->billpkgtaxlocationnum; # don't try to double-insert
208 # This cust_bill_pkg can be linked on either side (i.e. it can be the
209 # tax or the taxed item). If the other side is already inserted,
210 # then set billpkgnum to ours, and insert the link. Otherwise,
211 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
212 # on the other side, to be inserted later.
214 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
215 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
216 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
217 # break circular links when doing this
218 $link->set('tax_cust_bill_pkg', '');
220 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
221 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
222 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
223 # XXX if we ever do tax-on-tax for these, this will have to change
224 # since pkgnum will be zero
225 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
226 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
227 $link->set('taxable_cust_bill_pkg', '');
230 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
231 $error = $link->insert;
233 $dbh->rollback if $oldAutoCommit;
234 return "error inserting cust_bill_pkg_tax_location: $error";
238 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
239 : $link->get('tax_cust_bill_pkg');
240 my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
241 push @$link_array, $link;
242 $other->set('cust_bill_pkg_tax_location' => $link_array);
247 # someday you will be as awesome as cust_bill_pkg_tax_location...
249 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
250 if ( $tax_rate_location ) {
251 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
252 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
253 $error = $cust_bill_pkg_tax_rate_location->insert;
255 $dbh->rollback if $oldAutoCommit;
256 return "error inserting cust_bill_pkg_tax_rate_location: $error";
261 my $fee_links = $self->get('cust_bill_pkg_fee');
263 foreach my $link ( @$fee_links ) {
264 # very similar to cust_bill_pkg_tax_location, for obvious reasons
265 next if $link->billpkgfeenum; # don't try to double-insert
267 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
268 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
270 if ( $target and $target->billpkgnum ) {
271 $link->set('billpkgnum', $target->billpkgnum);
272 # base_invnum => null indicates that the fee is based on its own
274 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
275 $link->set('cust_bill_pkg', '');
278 if ( $base and $base->billpkgnum ) {
279 $link->set('base_billpkgnum', $base->billpkgnum);
280 $link->set('base_cust_bill_pkg', '');
282 # it's based on a line item that's not yet inserted
283 my $link_array = $base->get('cust_bill_pkg_fee') || [];
284 push @$link_array, $link;
285 $base->set('cust_bill_pkg_fee' => $link_array);
286 next; # don't insert the link yet
289 $error = $link->insert;
291 $dbh->rollback if $oldAutoCommit;
292 return "error inserting cust_bill_pkg_fee: $error";
297 my $cust_event_fee = $self->get('cust_event_fee');
298 if ( $cust_event_fee ) {
299 $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
300 $error = $cust_event_fee->replace;
302 $dbh->rollback if $oldAutoCommit;
303 return "error updating cust_event_fee: $error";
307 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
308 if ( $cust_tax_adjustment ) {
309 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
310 $error = $cust_tax_adjustment->replace;
312 $dbh->rollback if $oldAutoCommit;
313 return "error replacing cust_tax_adjustment: $error";
317 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
324 Voids this line item: deletes the line item and adds a record of the voided
325 line item to the FS::cust_bill_pkg_void table (and related tables).
331 my $reason = scalar(@_) ? shift : '';
333 local $SIG{HUP} = 'IGNORE';
334 local $SIG{INT} = 'IGNORE';
335 local $SIG{QUIT} = 'IGNORE';
336 local $SIG{TERM} = 'IGNORE';
337 local $SIG{TSTP} = 'IGNORE';
338 local $SIG{PIPE} = 'IGNORE';
340 my $oldAutoCommit = $FS::UID::AutoCommit;
341 local $FS::UID::AutoCommit = 0;
344 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
345 map { $_ => $self->get($_) } $self->fields
347 $cust_bill_pkg_void->reason($reason);
348 my $error = $cust_bill_pkg_void->insert;
350 $dbh->rollback if $oldAutoCommit;
354 foreach my $table (qw(
356 cust_bill_pkg_display
357 cust_bill_pkg_discount
358 cust_bill_pkg_tax_location
359 cust_bill_pkg_tax_rate_location
363 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
365 my $vclass = 'FS::'.$table.'_void';
366 my $void = $vclass->new( {
367 map { $_ => $linked->get($_) } $linked->fields
369 my $error = $void->insert || $linked->delete;
371 $dbh->rollback if $oldAutoCommit;
379 $error = $self->delete;
381 $dbh->rollback if $oldAutoCommit;
385 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
400 local $SIG{HUP} = 'IGNORE';
401 local $SIG{INT} = 'IGNORE';
402 local $SIG{QUIT} = 'IGNORE';
403 local $SIG{TERM} = 'IGNORE';
404 local $SIG{TSTP} = 'IGNORE';
405 local $SIG{PIPE} = 'IGNORE';
407 my $oldAutoCommit = $FS::UID::AutoCommit;
408 local $FS::UID::AutoCommit = 0;
411 foreach my $table (qw(
413 cust_bill_pkg_display
414 cust_bill_pkg_discount
415 cust_bill_pkg_tax_location
416 cust_bill_pkg_tax_rate_location
422 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
423 my $error = $linked->delete;
425 $dbh->rollback if $oldAutoCommit;
432 foreach my $cust_tax_adjustment (
433 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
435 $cust_tax_adjustment->billpkgnum(''); #NULL
436 my $error = $cust_tax_adjustment->replace;
438 $dbh->rollback if $oldAutoCommit;
443 my $error = $self->SUPER::delete(@_);
445 $dbh->rollback if $oldAutoCommit;
449 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
455 #alas, bin/follow-tax-rename
457 #=item replace OLD_RECORD
459 #Currently unimplemented. This would be even more of an accounting nightmare
460 #than deleteing the items. Just don't do it.
465 # return "Can't modify cust_bill_pkg records!";
470 Checks all fields to make sure this is a valid line item. If there is an
471 error, returns the error, otherwise returns false. Called by the insert
480 $self->ut_numbern('billpkgnum')
481 || $self->ut_snumber('pkgnum')
482 || $self->ut_number('invnum')
483 || $self->ut_money('setup')
484 || $self->ut_money('recur')
485 || $self->ut_numbern('sdate')
486 || $self->ut_numbern('edate')
487 || $self->ut_textn('itemdesc')
488 || $self->ut_textn('itemcomment')
489 || $self->ut_enum('hidden', [ '', 'Y' ])
491 return $error if $error;
493 $self->regularize_details;
495 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
496 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
497 return "Unknown pkgnum ". $self->pkgnum
498 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
501 return "Unknown invnum"
502 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
507 =item regularize_details
509 Converts the contents of the 'details' pseudo-field to
510 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
514 sub regularize_details {
516 if ( $self->get('details') ) {
517 foreach my $detail ( @{$self->get('details')} ) {
518 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
519 # then turn it into one
521 if ( ! ref($detail) ) {
522 $hash{'detail'} = $detail;
524 elsif ( ref($detail) eq 'HASH' ) {
527 elsif ( ref($detail) eq 'ARRAY' ) {
528 carp "passing invoice details as arrays is deprecated";
529 #carp "this way sucks, use a hash"; #but more useful/friendly
530 $hash{'format'} = $detail->[0];
531 $hash{'detail'} = $detail->[1];
532 $hash{'amount'} = $detail->[2];
533 $hash{'classnum'} = $detail->[3];
534 $hash{'phonenum'} = $detail->[4];
535 $hash{'accountcode'} = $detail->[5];
536 $hash{'startdate'} = $detail->[6];
537 $hash{'duration'} = $detail->[7];
538 $hash{'regionname'} = $detail->[8];
541 die "unknown detail type ". ref($detail);
543 $detail = new FS::cust_bill_pkg_detail \%hash;
545 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
553 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
559 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
564 Returns the customer (L<FS::cust_main> object) for this line item.
569 # required for cust_main_Mixin equivalence
570 # and use cust_bill instead of cust_pkg because this might not have a
573 my $cust_bill = $self->cust_bill or return '';
574 $cust_bill->cust_main;
577 =item previous_cust_bill_pkg
579 Returns the previous cust_bill_pkg for this package, if any.
583 sub previous_cust_bill_pkg {
585 return unless $self->sdate;
587 'table' => 'cust_bill_pkg',
588 'hashref' => { 'pkgnum' => $self->pkgnum,
589 'sdate' => { op=>'<', value=>$self->sdate },
591 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
597 Returns the amount owed (still outstanding) on this line item's setup fee,
598 which is the amount of the line item minus all payment applications (see
599 L<FS::cust_bill_pay_pkg> and credit applications (see
600 L<FS::cust_credit_bill_pkg>).
606 $self->owed('setup', @_);
611 Returns the amount owed (still outstanding) on this line item's recurring fee,
612 which is the amount of the line item minus all payment applications (see
613 L<FS::cust_bill_pay_pkg> and credit applications (see
614 L<FS::cust_credit_bill_pkg>).
620 $self->owed('recur', @_);
623 # modeled after cust_bill::owed...
625 my( $self, $field ) = @_;
626 my $balance = $self->$field();
627 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
628 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
629 $balance = sprintf( '%.2f', $balance );
630 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
636 my( $self, $field ) = @_;
637 my $balance = $self->$field();
638 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
639 $balance = sprintf( '%.2f', $balance );
640 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
644 sub cust_bill_pay_pkg {
645 my( $self, $field ) = @_;
646 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
647 'setuprecur' => $field,
652 sub cust_credit_bill_pkg {
653 my( $self, $field ) = @_;
654 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
655 'setuprecur' => $field,
662 Returns the number of billing units (for tax purposes) represented by this,
669 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
673 =item set_display OPTION => VALUE ...
675 A helper method for I<insert>, populates the pseudo-field B<display> with
676 appropriate FS::cust_bill_pkg_display objects.
678 Options are passed as a list of name/value pairs. Options are:
680 part_pkg: FS::part_pkg object from this line item's package.
682 real_pkgpart: if this line item comes from a bundled package, the pkgpart
683 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
688 my( $self, %opt ) = @_;
689 my $part_pkg = $opt{'part_pkg'};
690 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
692 my $conf = new FS::Conf;
694 # whether to break this down into setup/recur/usage
695 my $separate = $conf->exists('separate_usage');
697 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
698 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
700 # or use the category from $opt{'part_pkg'} if its not bundled?
701 my $categoryname = $cust_pkg->part_pkg->categoryname;
703 # if we don't have to separate setup/recur/usage, or put this in a
704 # package-specific section, or display a usage summary, then don't
705 # even create one of these. The item will just display in the unnamed
706 # section as a single line plus details.
707 return $self->set('display', [])
708 unless $separate || $categoryname || $usage_mandate;
712 my %hash = ( 'section' => $categoryname );
714 # whether to put usage details in a separate section, and if so, which one
715 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
716 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
718 # whether to show a usage summary line (total usage charges, no details)
719 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
720 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
723 # create lines for setup and (non-usage) recur, in the main section
724 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
725 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
727 # display everything in a single line
728 push @display, new FS::cust_bill_pkg_display
731 # and if usage_mandate is enabled, hide details
732 # (this only works on multisection invoices...)
733 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
737 if ($separate && $usage_section && $summary) {
738 # create a line for the usage summary in the main section
739 push @display, new FS::cust_bill_pkg_display { type => 'U',
745 if ($usage_mandate || ($usage_section && $summary) ) {
746 $hash{post_total} = 'Y';
749 if ($separate || $usage_mandate) {
750 # show call details for this line item in the usage section.
751 # if usage_mandate is on, this will display below the section subtotal.
752 # this also happens if usage is in a separate section and there's a
753 # summary in the main section, though I'm not sure why.
754 $hash{section} = $usage_section if $usage_section;
755 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
758 $self->set('display', \@display);
764 Returns a hash: keys are "setup", "recur" or usage classnum, values are
765 FS::cust_bill_pkg objects, each with no more than a single class (setup or
772 # XXX this goes away with cust_bill_pkg refactor
774 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
775 my %cust_bill_pkg = ();
777 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
778 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
781 #split setup and recur
782 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
783 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
784 $cust_bill_pkg->set('details', []);
785 $cust_bill_pkg->recur(0);
786 $cust_bill_pkg->unitrecur(0);
787 $cust_bill_pkg->type('');
788 $cust_bill_pkg_recur->setup(0);
789 $cust_bill_pkg_recur->unitsetup(0);
790 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
794 #split usage from recur
795 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
796 if exists($cust_bill_pkg{recur});
797 warn "usage is $usage\n" if $DEBUG > 1;
799 my $cust_bill_pkg_usage =
800 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
801 $cust_bill_pkg_usage->recur( $usage );
802 $cust_bill_pkg_usage->type( 'U' );
803 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
804 $cust_bill_pkg{recur}->recur( $recur );
805 $cust_bill_pkg{recur}->type( '' );
806 $cust_bill_pkg{recur}->set('details', []);
807 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
810 #subdivide usage by usage_class
811 if (exists($cust_bill_pkg{''})) {
812 foreach my $class (grep { $_ } $self->usage_classes) {
813 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
814 my $cust_bill_pkg_usage =
815 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
816 $cust_bill_pkg_usage->recur( $usage );
817 $cust_bill_pkg_usage->set('details', []);
818 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
819 $cust_bill_pkg{''}->recur( $classless );
820 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
822 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
823 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
824 delete $cust_bill_pkg{''}
825 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
828 # # sort setup,recur,'', and the rest numeric && return
829 # my @result = map { $cust_bill_pkg{$_} }
830 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
831 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
833 # keys %cust_bill_pkg;
842 Returns the amount of the charge associated with usage class CLASSNUM if
843 CLASSNUM is defined. Otherwise returns the total charge associated with
849 my( $self, $classnum ) = @_;
850 $self->regularize_details;
852 if ( $self->get('details') ) {
855 map { $_->amount || 0 }
856 grep { !defined($classnum) or $classnum eq $_->classnum }
857 @{ $self->get('details') }
862 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
863 ' WHERE billpkgnum = '. $self->billpkgnum;
864 $sql .= " AND classnum = $classnum" if defined($classnum);
866 my $sth = dbh->prepare($sql) or die dbh->errstr;
867 $sth->execute or die $sth->errstr;
869 return $sth->fetchrow_arrayref->[0] || 0;
877 Returns a list of usage classnums associated with this invoice line's
884 $self->regularize_details;
886 if ( $self->get('details') ) {
888 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
894 qsearch({ table => 'cust_bill_pkg_detail',
895 hashref => { billpkgnum => $self->billpkgnum },
896 select => 'DISTINCT classnum',
903 sub cust_tax_exempt_pkg {
906 $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
909 =item cust_bill_pkg_fee
911 Returns the list of associated cust_bill_pkg_fee objects, if this is
916 sub cust_bill_pkg_fee {
918 qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
921 =item cust_bill_pkg_tax_Xlocation
923 Returns the list of associated cust_bill_pkg_tax_location and/or
924 cust_bill_pkg_tax_rate_location objects
928 sub cust_bill_pkg_tax_Xlocation {
931 my %hash = ( 'billpkgnum' => $self->billpkgnum );
934 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
935 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
940 =item recur_show_zero
944 sub recur_show_zero { shift->_X_show_zero('recur'); }
945 sub setup_show_zero { shift->_X_show_zero('setup'); }
948 my( $self, $what ) = @_;
950 return 0 unless $self->$what() == 0 && $self->pkgnum;
952 $self->cust_pkg->_X_show_zero($what);
955 =item credited [ BEFORE, AFTER, OPTIONS ]
957 Returns the sum of credits applied to this item. Arguments are the same as
958 owed_sql/paid_sql/credited_sql.
964 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
967 =item tax_locationnum
969 Returns the L<FS::cust_location> number that this line item is in for tax
970 purposes. For package sales, it's the package tax location; for fees,
971 it's the customer's default service location.
975 sub tax_locationnum {
977 if ( $self->pkgnum ) { # normal sales
978 return $self->cust_pkg->tax_locationnum;
979 } elsif ( $self->feepart ) { # fees
980 return $self->cust_bill->cust_main->ship_locationnum;
988 if ( $self->pkgnum ) { # normal sales
989 return $self->cust_pkg->tax_location;
990 } elsif ( $self->feepart ) { # fees
991 return $self->cust_bill->cust_main->ship_location;
999 Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
1000 charge. If called on a tax line, returns nothing.
1006 if ( $self->pkgpart_override ) {
1007 return FS::part_pkg->by_key($self->pkgpart_override);
1008 } elsif ( $self->pkgnum ) {
1009 return $self->cust_pkg->part_pkg;
1010 } elsif ( $self->feepart ) {
1011 return $self->part_fee;
1022 ? FS::part_fee->by_key($self->feepart)
1028 =head1 CLASS METHODS
1034 Returns an SQL expression for the total usage charges in details on
1040 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1041 FROM cust_bill_pkg_detail
1042 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1044 sub usage_sql { $usage_sql }
1046 # this makes owed_sql, etc. much more concise
1048 my ($class, $start, $end, %opt) = @_;
1049 my $setuprecur = $opt{setuprecur} || '';
1051 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1052 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1053 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1055 if ($opt{no_usage} and $charged =~ /recur/) {
1056 $charged = "$charged - $usage_sql"
1063 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1065 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1066 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1067 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1073 '(' . $class->charged_sql(@_) .
1074 ' - ' . $class->paid_sql(@_) .
1075 ' - ' . $class->credited_sql(@_) . ')'
1078 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1080 Returns an SQL expression for the sum of payments applied to this item.
1085 my ($class, $start, $end, %opt) = @_;
1086 my $s = $start ? "AND cust_pay._date <= $start" : '';
1087 my $e = $end ? "AND cust_pay._date > $end" : '';
1088 my $setuprecur = $opt{setuprecur} || '';
1089 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1090 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1091 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1093 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1094 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1095 JOIN cust_pay USING (paynum)
1096 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1097 $s $e $setuprecur )";
1099 if ( $opt{no_usage} ) {
1100 # cap the amount paid at the sum of non-usage charges,
1101 # minus the amount credited against non-usage charges
1103 $class->charged_sql($start, $end, %opt) . ' - ' .
1104 $class->credited_sql($start, $end, %opt).')';
1113 my ($class, $start, $end, %opt) = @_;
1114 my $s = $start ? "AND cust_credit._date <= $start" : '';
1115 my $e = $end ? "AND cust_credit._date > $end" : '';
1116 my $setuprecur = $opt{setuprecur} || '';
1117 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1118 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1119 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1121 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1122 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1123 JOIN cust_credit USING (crednum)
1124 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1125 $s $e $setuprecur )";
1127 if ( $opt{no_usage} ) {
1128 # cap the amount credited at the sum of non-usage charges
1129 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1137 sub upgrade_tax_location {
1138 # For taxes that were calculated/invoiced before cust_location refactoring
1139 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1140 # they were calculated on a package-location basis. Create them here,
1141 # along with any necessary cust_location records and any tax exemption
1144 my ($class, %opt) = @_;
1145 # %opt may include 's' and 'e': start and end date ranges
1146 # and 'X': abort on any error, instead of just rolling back changes to
1149 my $oldAutoCommit = $FS::UID::AutoCommit;
1150 local $FS::UID::AutoCommit = 0;
1153 use FS::h_cust_main;
1154 use FS::h_cust_bill;
1156 use FS::h_cust_main_exemption;
1159 local $FS::cust_location::import = 1;
1161 my $conf = FS::Conf->new; # h_conf?
1162 return if $conf->exists('enable_taxproducts'); #don't touch this case
1163 my $use_ship = $conf->exists('tax-ship_address');
1165 my $date_where = '';
1167 $date_where .= " AND cust_bill._date >= $opt{s}";
1170 $date_where .= " AND cust_bill._date < $opt{e}";
1173 my $commit_each_invoice = 1 unless $opt{X};
1175 # if an invoice has either of these kinds of objects, then it doesn't
1176 # need to be upgraded...probably
1177 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1178 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1179 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1180 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1181 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1182 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1183 ' AND exempt_monthly IS NULL';
1185 my @invnums = map { $_->invnum } qsearch({
1186 select => 'cust_bill.invnum',
1187 table => 'cust_bill',
1189 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1190 "AND NOT EXISTS($sub_has_exempt) ".
1194 print "Processing ".scalar(@invnums)." invoices...\n";
1198 foreach my $invnum (@invnums) {
1200 print STDERR "Invoice #$invnum\n";
1202 my %pkgpart_taxclass; # pkgpart => taxclass
1203 my %pkgpart_exempt_setup;
1204 my %pkgpart_exempt_recur;
1205 my $h_cust_bill = qsearchs('h_cust_bill',
1206 { invnum => $invnum,
1207 history_action => 'insert' });
1208 if (!$h_cust_bill) {
1209 warn "no insert record for invoice $invnum; skipped\n";
1210 #$date = $cust_bill->_date as a fallback?
1211 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1212 # when looking up history records in other tables.
1215 my $custnum = $h_cust_bill->custnum;
1217 # Determine the address corresponding to this tax region.
1218 # It's either the bill or ship address of the customer as of the
1219 # invoice date-of-insertion. (Not necessarily the invoice date.)
1220 my $date = $h_cust_bill->history_date;
1221 my $h_cust_main = qsearchs('h_cust_main',
1222 { custnum => $custnum },
1223 FS::h_cust_main->sql_h_searchs($date)
1225 if (!$h_cust_main ) {
1226 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1228 # fallback to current $cust_main? sounds dangerous.
1231 # This is a historical customer record, so it has a historical address.
1232 # If there's no cust_location matching this custnum and address (there
1233 # probably isn't), create one.
1234 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1235 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1236 FS::cust_main->location_fields;
1237 # not really needed for this, and often result in duplicate locations
1238 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1240 $hash{custnum} = $h_cust_main->custnum;
1241 my $tax_loc = FS::cust_location->new(\%hash);
1242 my $error = $tax_loc->find_or_insert || $tax_loc->disable_if_unused;
1244 warn "couldn't create historical location record for cust#".
1245 $h_cust_main->custnum.": $error\n";
1248 my $exempt_cust = 1 if $h_cust_main->tax;
1250 # Get any per-customer taxname exemptions that were in effect.
1251 my %exempt_cust_taxname = map {
1253 } qsearch('h_cust_main_exemption', { 'custnum' => $custnum },
1254 FS::h_cust_main_exemption->sql_h_searchs($date)
1257 # classify line items
1259 my %nontax_items; # taxclass => array of cust_bill_pkg
1260 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1261 my $pkgnum = $item->pkgnum;
1263 if ( $pkgnum == 0 ) {
1265 push @tax_items, $item;
1268 # (pkgparts really shouldn't change, right?)
1269 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1270 FS::h_cust_pkg->sql_h_searchs($date)
1272 if ( !$h_cust_pkg ) {
1273 warn "no historical package #".$item->pkgpart."; skipped\n";
1276 my $pkgpart = $h_cust_pkg->pkgpart;
1278 if (!exists $pkgpart_taxclass{$pkgpart}) {
1279 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1280 FS::h_part_pkg->sql_h_searchs($date)
1282 if ( !$h_part_pkg ) {
1283 warn "no historical package def #$pkgpart; skipped\n";
1286 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1287 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1288 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1291 # mark any exemptions that apply
1292 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1293 $item->set('exempt_setup' => 1);
1296 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1297 $item->set('exempt_recur' => 1);
1300 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1302 $nontax_items{$taxclass} ||= [];
1303 push @{ $nontax_items{$taxclass} }, $item;
1306 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1309 # Use a variation on the procedure in
1310 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1312 my @loc_keys = qw( district city county state country );
1313 my %taxhash = map { $_ => $h_cust_main->get($pre.$_) } @loc_keys;
1314 my %taxdef_by_name; # by name, and then by taxclass
1315 my %est_tax; # by name, and then by taxclass
1316 my %taxable_items; # by taxnum, and then an array
1318 foreach my $taxclass (keys %nontax_items) {
1319 my %myhash = %taxhash;
1320 my @elim = qw( district city county state );
1321 my @taxdefs; # because there may be several with different taxnames
1323 $myhash{taxclass} = $taxclass;
1324 @taxdefs = qsearch('cust_main_county', \%myhash);
1326 $myhash{taxclass} = '';
1327 @taxdefs = qsearch('cust_main_county', \%myhash);
1329 $myhash{ shift @elim } = '';
1330 } while scalar(@elim) and !@taxdefs;
1332 print "Class '$taxclass': ". scalar(@{ $nontax_items{$taxclass} }).
1333 " items, ". scalar(@taxdefs)." tax defs found.\n";
1334 foreach my $taxdef (@taxdefs) {
1335 next if $taxdef->tax == 0;
1336 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1338 $taxable_items{$taxdef->taxnum} ||= [];
1339 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1340 # clone the item so that taxdef-dependent changes don't
1341 # change it for other taxdefs
1342 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1344 # these flags are already set if the part_pkg declares itself exempt
1345 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1346 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1349 my $taxable = $item->setup + $item->recur;
1351 # h_cust_credit_bill_pkg?
1352 # NO. Because if these exemptions HAD been created at the time of
1353 # billing, and then a credit applied later, the exemption would
1354 # have been adjusted by the amount of the credit. So we adjust
1355 # the taxable amount before creating the exemption.
1356 # But don't deduct the credit from taxable, because the tax was
1357 # calculated before the credit was applied.
1358 foreach my $f (qw(setup recur)) {
1359 my $credited = FS::Record->scalar_sql(
1360 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1361 "WHERE billpkgnum = ? AND setuprecur = ?",
1365 $item->set($f, $item->get($f) - $credited) if $credited;
1367 my $existing_exempt = FS::Record->scalar_sql(
1368 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1369 "billpkgnum = ? AND taxnum = ?",
1370 $item->billpkgnum, $taxdef->taxnum
1372 $taxable -= $existing_exempt;
1374 if ( $taxable and $exempt_cust ) {
1375 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1378 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1379 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1382 if ( $taxable and $item->exempt_setup ) {
1383 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1384 $taxable -= $item->setup;
1386 if ( $taxable and $item->exempt_recur ) {
1387 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1388 $taxable -= $item->recur;
1391 $item->set('taxable' => $taxable);
1392 push @{ $taxable_items{$taxdef->taxnum} }, $item
1395 # estimate the amount of tax (this is necessary because different
1396 # taxdefs with the same taxname may have different tax rates)
1397 # and sum that for each taxname/taxclass combination
1399 $est_tax{$taxdef->taxname} ||= {};
1400 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1401 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1402 $taxable * $taxdef->tax;
1404 foreach (@new_exempt) {
1405 next if $_->{amount} == 0;
1406 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1408 billpkgnum => $item->billpkgnum,
1409 taxnum => $taxdef->taxnum,
1411 my $error = $cust_tax_exempt_pkg->insert;
1413 my $pkgnum = $item->pkgnum;
1414 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1418 } #foreach @new_exempt
1421 } #foreach $taxclass
1423 # Now go through the billed taxes and match them up with the line items.
1424 TAX_ITEM: foreach my $tax_item ( @tax_items )
1426 my $taxname = $tax_item->itemdesc;
1427 $taxname = '' if $taxname eq 'Tax';
1429 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1430 # then we didn't find any applicable taxes with this name
1431 warn "no definition found for tax item '$taxname'.\n".
1432 '('.join(' ', @hash{qw(country state county city district)}).")\n";
1433 # possibly all of these should be "next TAX_ITEM", but whole invoices
1434 # are transaction protected and we can go back and retry them.
1437 # classname => cust_main_county
1438 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1440 # Divide the tax item among taxclasses, if necessary
1441 # classname => estimated tax amount
1442 my $this_est_tax = $est_tax{$taxname};
1443 if (!defined $this_est_tax) {
1444 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1447 my $est_total = sum(values %$this_est_tax);
1448 if ( $est_total == 0 ) {
1450 warn "estimated tax on invoice #$invnum is zero.\n";
1454 my $real_tax = $tax_item->setup;
1455 printf ("Distributing \$%.2f tax:\n", $real_tax);
1456 my $cents_remaining = $real_tax * 100; # for rounding error
1457 my @tax_links; # partial CBPTL hashrefs
1458 foreach my $taxclass (keys %taxdef_by_class) {
1459 my $taxdef = $taxdef_by_class{$taxclass};
1460 # these items already have "taxable" set to their charge amount
1461 # after applying any credits or exemptions
1462 my @items = @{ $taxable_items{$taxdef->taxnum} };
1463 my $subtotal = sum(map {$_->get('taxable')} @items);
1464 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1466 foreach my $nontax (@items) {
1467 my $part = int($real_tax
1469 * ($this_est_tax->{$taxclass}/$est_total)
1471 * ($nontax->get('taxable'))/$subtotal
1475 $cents_remaining -= $part;
1477 taxnum => $taxdef->taxnum,
1478 pkgnum => $nontax->pkgnum,
1479 billpkgnum => $nontax->billpkgnum,
1483 } #foreach $taxclass
1484 # Distribute any leftover tax round-robin style, one cent at a time.
1486 my $nlinks = scalar(@tax_links);
1488 while (int($cents_remaining) > 0) {
1489 $tax_links[$i % $nlinks]->{cents} += 1;
1494 warn "Can't create tax links--no taxable items found.\n";
1498 # Gather credit/payment applications so that we can link them
1501 qsearch( 'cust_credit_bill_pkg',
1502 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1504 qsearch( 'cust_bill_pay_pkg',
1505 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1509 # grab the first one
1510 my $this_unlinked = shift @unlinked;
1511 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1513 # Create tax links (yay!)
1514 printf("Creating %d tax links.\n",scalar(@tax_links));
1515 foreach (@tax_links) {
1516 my $link = FS::cust_bill_pkg_tax_location->new({
1517 billpkgnum => $tax_item->billpkgnum,
1518 taxtype => 'FS::cust_main_county',
1519 locationnum => $tax_loc->locationnum,
1520 taxnum => $_->{taxnum},
1521 pkgnum => $_->{pkgnum},
1522 amount => sprintf('%.2f', $_->{cents} / 100),
1523 taxable_billpkgnum => $_->{billpkgnum},
1525 my $error = $link->insert;
1527 warn "Can't create tax link for inv#$invnum: $error\n";
1531 my $link_cents = $_->{cents};
1532 # update/create subitem links
1534 # If $this_unlinked is undef, then we've allocated all of the
1535 # credit/payment applications to the tax item. If $link_cents is 0,
1536 # then we've applied credits/payments to all of this package fraction,
1537 # so go on to the next.
1538 while ($this_unlinked and $link_cents) {
1539 # apply as much as possible of $link_amount to this credit/payment
1541 my $apply_cents = min($link_cents, $unlinked_cents);
1542 $link_cents -= $apply_cents;
1543 $unlinked_cents -= $apply_cents;
1544 # $link_cents or $unlinked_cents or both are now zero
1545 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1546 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1547 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1548 if ( $this_unlinked->$pkey ) {
1549 # then it's an existing link--replace it
1550 $error = $this_unlinked->replace;
1552 $this_unlinked->insert;
1554 # what do we do with errors at this stage?
1556 warn "Error creating tax application link: $error\n";
1557 next INVOICE; # for lack of a better idea
1560 if ( $unlinked_cents == 0 ) {
1561 # then we've allocated all of this payment/credit application,
1562 # so grab the next one
1563 $this_unlinked = shift @unlinked;
1564 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1565 } elsif ( $link_cents == 0 ) {
1566 # then we've covered all of this package tax fraction, so split
1567 # off a new application from this one
1568 $this_unlinked = $this_unlinked->new({
1569 $this_unlinked->hash,
1572 # $unlinked_cents is still what it is
1575 } #while $this_unlinked and $link_cents
1576 } #foreach (@tax_links)
1577 } #foreach $tax_item
1579 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1585 $dbh->rollback if $oldAutoCommit;
1586 die "Upgrade halted.\n" unless $commit_each_invoice;
1590 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1595 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1599 use Date::Parse 'str2time';
1602 my $upgrade = 'tax_location_2012';
1603 return if FS::upgrade_journal->is_done($upgrade);
1604 my $job = FS::queue->new({
1605 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1607 # call it kind of like a class method, not that it matters much
1608 $job->insert($class, 's' => str2time('2012-01-01'));
1609 # if there's a customer location upgrade queued also, wait for it to
1611 my $location_job = qsearchs('queue', {
1612 job => 'FS::cust_main::Location::process_upgrade_location'
1614 if ( $location_job ) {
1615 $job->depend_insert($location_job->jobnum);
1617 # Then mark the upgrade as done, so that we don't queue the job twice
1618 # and somehow run two of them concurrently.
1619 FS::upgrade_journal->set_done($upgrade);
1620 # This upgrade now does the job of assigning taxable_billpkgnums to
1621 # cust_bill_pkg_tax_location, so set that task done also.
1622 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1629 setup and recur shouldn't be separate fields. There should be one "amount"
1630 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1632 A line item with both should really be two separate records (preserving
1633 sdate and edate for setup fees for recurring packages - that information may
1634 be valuable later). Invoice generation (cust_main::bill), invoice printing
1635 (cust_bill), tax reports (report_tax.cgi) and line item reports
1636 (cust_bill_pkg.cgi) would need to be updated.
1638 owed_setup and owed_recur could then be repaced by just owed, and
1639 cust_bill::open_cust_bill_pkg and
1640 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1642 The upgrade procedure is pretty sketchy.
1646 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1647 from the base documentation.