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 );
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pkg_discount;
14 use FS::cust_bill_pkg_fee;
15 use FS::cust_bill_pay_pkg;
16 use FS::cust_credit_bill_pkg;
17 use FS::cust_tax_exempt_pkg;
18 use FS::cust_bill_pkg_tax_location;
19 use FS::cust_bill_pkg_tax_rate_location;
20 use FS::cust_tax_adjustment;
21 use FS::cust_bill_pkg_void;
22 use FS::cust_bill_pkg_detail_void;
23 use FS::cust_bill_pkg_display_void;
24 use FS::cust_bill_pkg_discount_void;
25 use FS::cust_bill_pkg_tax_location_void;
26 use FS::cust_bill_pkg_tax_rate_location_void;
27 use FS::cust_tax_exempt_pkg_void;
30 $me = '[FS::cust_bill_pkg]';
34 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
38 use FS::cust_bill_pkg;
40 $record = new FS::cust_bill_pkg \%hash;
41 $record = new FS::cust_bill_pkg { 'column' => 'value' };
43 $error = $record->insert;
45 $error = $record->check;
49 An FS::cust_bill_pkg object represents an invoice line item.
50 FS::cust_bill_pkg inherits from FS::Record. The following fields are
61 invoice (see L<FS::cust_bill>)
65 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)
67 =item pkgpart_override
69 optional package definition (see L<FS::part_pkg>) override
81 starting date of recurring fee
85 ending date of recurring fee
89 Line item description (overrides normal package description)
93 If not set, defaults to 1
97 If not set, defaults to setup
101 If not set, defaults to recur
105 If set to Y, indicates data should not appear as separate line item on invoice
109 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
110 see L<Time::Local> and L<Date::Parse> for conversion functions.
118 Creates a new line item. To add the line item to the database, see
119 L<"insert">. Line items are normally created by calling the bill method of a
120 customer object (see L<FS::cust_main>).
124 sub table { 'cust_bill_pkg'; }
126 sub detail_table { 'cust_bill_pkg_detail'; }
127 sub display_table { 'cust_bill_pkg_display'; }
128 sub discount_table { 'cust_bill_pkg_discount'; }
129 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
130 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
131 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
135 Adds this line item to the database. If there is an error, returns the error,
136 otherwise returns false.
143 local $SIG{HUP} = 'IGNORE';
144 local $SIG{INT} = 'IGNORE';
145 local $SIG{QUIT} = 'IGNORE';
146 local $SIG{TERM} = 'IGNORE';
147 local $SIG{TSTP} = 'IGNORE';
148 local $SIG{PIPE} = 'IGNORE';
150 my $oldAutoCommit = $FS::UID::AutoCommit;
151 local $FS::UID::AutoCommit = 0;
154 my $error = $self->SUPER::insert;
156 $dbh->rollback if $oldAutoCommit;
160 if ( $self->get('details') ) {
161 foreach my $detail ( @{$self->get('details')} ) {
162 $detail->billpkgnum($self->billpkgnum);
163 $error = $detail->insert;
165 $dbh->rollback if $oldAutoCommit;
166 return "error inserting cust_bill_pkg_detail: $error";
171 if ( $self->get('display') ) {
172 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
173 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
174 $error = $cust_bill_pkg_display->insert;
176 $dbh->rollback if $oldAutoCommit;
177 return "error inserting cust_bill_pkg_display: $error";
182 if ( $self->get('discounts') ) {
183 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
184 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
185 $error = $cust_bill_pkg_discount->insert;
187 $dbh->rollback if $oldAutoCommit;
188 return "error inserting cust_bill_pkg_discount: $error";
193 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
194 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
195 $error = $cust_tax_exempt_pkg->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "error inserting cust_tax_exempt_pkg: $error";
202 my $tax_location = $self->get('cust_bill_pkg_tax_location');
203 if ( $tax_location ) {
204 foreach my $link ( @$tax_location ) {
205 next if $link->billpkgtaxlocationnum; # don't try to double-insert
206 # This cust_bill_pkg can be linked on either side (i.e. it can be the
207 # tax or the taxed item). If the other side is already inserted,
208 # then set billpkgnum to ours, and insert the link. Otherwise,
209 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
210 # on the other side, to be inserted later.
212 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
213 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
214 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
215 # break circular links when doing this
216 $link->set('tax_cust_bill_pkg', '');
218 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
219 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
220 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
221 # XXX if we ever do tax-on-tax for these, this will have to change
222 # since pkgnum will be zero
223 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
224 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
225 $link->set('taxable_cust_bill_pkg', '');
228 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
229 $error = $link->insert;
231 $dbh->rollback if $oldAutoCommit;
232 return "error inserting cust_bill_pkg_tax_location: $error";
236 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
237 : $link->get('tax_cust_bill_pkg');
238 my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
239 push @$link_array, $link;
240 $other->set('cust_bill_pkg_tax_location' => $link_array);
245 # someday you will be as awesome as cust_bill_pkg_tax_location...
247 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
248 if ( $tax_rate_location ) {
249 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
250 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
251 $error = $cust_bill_pkg_tax_rate_location->insert;
253 $dbh->rollback if $oldAutoCommit;
254 return "error inserting cust_bill_pkg_tax_rate_location: $error";
259 my $fee_links = $self->get('cust_bill_pkg_fee');
261 foreach my $link ( @$fee_links ) {
262 # very similar to cust_bill_pkg_tax_location, for obvious reasons
263 next if $link->billpkgfeenum; # don't try to double-insert
265 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
266 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
268 if ( $target and $target->billpkgnum ) {
269 $link->set('billpkgnum', $target->billpkgnum);
270 # base_invnum => null indicates that the fee is based on its own
272 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
273 $link->set('cust_bill_pkg', '');
276 if ( $base and $base->billpkgnum ) {
277 $link->set('base_billpkgnum', $base->billpkgnum);
278 $link->set('base_cust_bill_pkg', '');
280 # it's based on a line item that's not yet inserted
281 my $link_array = $base->get('cust_bill_pkg_fee') || [];
282 push @$link_array, $link;
283 $base->set('cust_bill_pkg_fee' => $link_array);
284 next; # don't insert the link yet
287 $error = $link->insert;
289 $dbh->rollback if $oldAutoCommit;
290 return "error inserting cust_bill_pkg_fee: $error";
295 my $cust_event_fee = $self->get('cust_event_fee');
296 if ( $cust_event_fee ) {
297 $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
298 $error = $cust_event_fee->replace;
300 $dbh->rollback if $oldAutoCommit;
301 return "error updating cust_event_fee: $error";
305 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
306 if ( $cust_tax_adjustment ) {
307 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
308 $error = $cust_tax_adjustment->replace;
310 $dbh->rollback if $oldAutoCommit;
311 return "error replacing cust_tax_adjustment: $error";
315 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
322 Voids this line item: deletes the line item and adds a record of the voided
323 line item to the FS::cust_bill_pkg_void table (and related tables).
329 my $reason = scalar(@_) ? shift : '';
331 local $SIG{HUP} = 'IGNORE';
332 local $SIG{INT} = 'IGNORE';
333 local $SIG{QUIT} = 'IGNORE';
334 local $SIG{TERM} = 'IGNORE';
335 local $SIG{TSTP} = 'IGNORE';
336 local $SIG{PIPE} = 'IGNORE';
338 my $oldAutoCommit = $FS::UID::AutoCommit;
339 local $FS::UID::AutoCommit = 0;
342 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
343 map { $_ => $self->get($_) } $self->fields
345 $cust_bill_pkg_void->reason($reason);
346 my $error = $cust_bill_pkg_void->insert;
348 $dbh->rollback if $oldAutoCommit;
352 foreach my $table (qw(
354 cust_bill_pkg_display
355 cust_bill_pkg_discount
356 cust_bill_pkg_tax_location
357 cust_bill_pkg_tax_rate_location
361 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
363 my $vclass = 'FS::'.$table.'_void';
364 my $void = $vclass->new( {
365 map { $_ => $linked->get($_) } $linked->fields
367 my $error = $void->insert || $linked->delete;
369 $dbh->rollback if $oldAutoCommit;
377 $error = $self->delete;
379 $dbh->rollback if $oldAutoCommit;
383 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
398 local $SIG{HUP} = 'IGNORE';
399 local $SIG{INT} = 'IGNORE';
400 local $SIG{QUIT} = 'IGNORE';
401 local $SIG{TERM} = 'IGNORE';
402 local $SIG{TSTP} = 'IGNORE';
403 local $SIG{PIPE} = 'IGNORE';
405 my $oldAutoCommit = $FS::UID::AutoCommit;
406 local $FS::UID::AutoCommit = 0;
409 foreach my $table (qw(
411 cust_bill_pkg_display
412 cust_bill_pkg_discount
413 cust_bill_pkg_tax_location
414 cust_bill_pkg_tax_rate_location
420 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
421 my $error = $linked->delete;
423 $dbh->rollback if $oldAutoCommit;
430 foreach my $cust_tax_adjustment (
431 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
433 $cust_tax_adjustment->billpkgnum(''); #NULL
434 my $error = $cust_tax_adjustment->replace;
436 $dbh->rollback if $oldAutoCommit;
441 my $error = $self->SUPER::delete(@_);
443 $dbh->rollback if $oldAutoCommit;
447 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
453 #alas, bin/follow-tax-rename
455 #=item replace OLD_RECORD
457 #Currently unimplemented. This would be even more of an accounting nightmare
458 #than deleteing the items. Just don't do it.
463 # return "Can't modify cust_bill_pkg records!";
468 Checks all fields to make sure this is a valid line item. If there is an
469 error, returns the error, otherwise returns false. Called by the insert
478 $self->ut_numbern('billpkgnum')
479 || $self->ut_snumber('pkgnum')
480 || $self->ut_number('invnum')
481 || $self->ut_money('setup')
482 || $self->ut_moneyn('unitsetup')
483 || $self->ut_currencyn('setup_billed_currency')
484 || $self->ut_moneyn('setup_billed_amount')
485 || $self->ut_money('recur')
486 || $self->ut_moneyn('unitrecur')
487 || $self->ut_currencyn('recur_billed_currency')
488 || $self->ut_moneyn('recur_billed_amount')
489 || $self->ut_numbern('sdate')
490 || $self->ut_numbern('edate')
491 || $self->ut_textn('itemdesc')
492 || $self->ut_textn('itemcomment')
493 || $self->ut_enum('hidden', [ '', 'Y' ])
495 return $error if $error;
497 $self->regularize_details;
499 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
500 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
501 return "Unknown pkgnum ". $self->pkgnum
502 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
505 return "Unknown invnum"
506 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
511 =item regularize_details
513 Converts the contents of the 'details' pseudo-field to
514 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
518 sub regularize_details {
520 if ( $self->get('details') ) {
521 foreach my $detail ( @{$self->get('details')} ) {
522 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
523 # then turn it into one
525 if ( ! ref($detail) ) {
526 $hash{'detail'} = $detail;
528 elsif ( ref($detail) eq 'HASH' ) {
531 elsif ( ref($detail) eq 'ARRAY' ) {
532 carp "passing invoice details as arrays is deprecated";
533 #carp "this way sucks, use a hash"; #but more useful/friendly
534 $hash{'format'} = $detail->[0];
535 $hash{'detail'} = $detail->[1];
536 $hash{'amount'} = $detail->[2];
537 $hash{'classnum'} = $detail->[3];
538 $hash{'phonenum'} = $detail->[4];
539 $hash{'accountcode'} = $detail->[5];
540 $hash{'startdate'} = $detail->[6];
541 $hash{'duration'} = $detail->[7];
542 $hash{'regionname'} = $detail->[8];
545 die "unknown detail type ". ref($detail);
547 $detail = new FS::cust_bill_pkg_detail \%hash;
549 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
557 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
561 Returns the customer (L<FS::cust_main> object) for this line item.
566 # required for cust_main_Mixin equivalence
567 # and use cust_bill instead of cust_pkg because this might not have a
570 my $cust_bill = $self->cust_bill or return '';
571 $cust_bill->cust_main;
574 =item previous_cust_bill_pkg
576 Returns the previous cust_bill_pkg for this package, if any.
580 sub previous_cust_bill_pkg {
582 return unless $self->sdate;
584 'table' => 'cust_bill_pkg',
585 'hashref' => { 'pkgnum' => $self->pkgnum,
586 'sdate' => { op=>'<', value=>$self->sdate },
588 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
594 Returns the amount owed (still outstanding) on this line item's setup fee,
595 which is the amount of the line item minus all payment applications (see
596 L<FS::cust_bill_pay_pkg> and credit applications (see
597 L<FS::cust_credit_bill_pkg>).
603 $self->owed('setup', @_);
608 Returns the amount owed (still outstanding) on this line item's recurring fee,
609 which is the amount of the line item minus all payment applications (see
610 L<FS::cust_bill_pay_pkg> and credit applications (see
611 L<FS::cust_credit_bill_pkg>).
617 $self->owed('recur', @_);
620 # modeled after cust_bill::owed...
622 my( $self, $field ) = @_;
623 my $balance = $self->$field();
624 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
625 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
626 $balance = sprintf( '%.2f', $balance );
627 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
633 my( $self, $field ) = @_;
634 my $balance = $self->$field();
635 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
636 $balance = sprintf( '%.2f', $balance );
637 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
641 sub cust_bill_pay_pkg {
642 my( $self, $field ) = @_;
643 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
644 'setuprecur' => $field,
649 sub cust_credit_bill_pkg {
650 my( $self, $field ) = @_;
651 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
652 'setuprecur' => $field,
659 Returns the number of billing units (for tax purposes) represented by this,
666 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
670 =item set_display OPTION => VALUE ...
672 A helper method for I<insert>, populates the pseudo-field B<display> with
673 appropriate FS::cust_bill_pkg_display objects.
675 Options are passed as a list of name/value pairs. Options are:
677 part_pkg: FS::part_pkg object from this line item's package.
679 real_pkgpart: if this line item comes from a bundled package, the pkgpart
680 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
685 my( $self, %opt ) = @_;
686 my $part_pkg = $opt{'part_pkg'};
687 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
689 my $conf = new FS::Conf;
691 # whether to break this down into setup/recur/usage
692 my $separate = $conf->exists('separate_usage');
694 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
695 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
697 # or use the category from $opt{'part_pkg'} if its not bundled?
698 my $categoryname = $cust_pkg->part_pkg->categoryname;
700 # if we don't have to separate setup/recur/usage, or put this in a
701 # package-specific section, or display a usage summary, then don't
702 # even create one of these. The item will just display in the unnamed
703 # section as a single line plus details.
704 return $self->set('display', [])
705 unless $separate || $categoryname || $usage_mandate;
709 my %hash = ( 'section' => $categoryname );
711 # whether to put usage details in a separate section, and if so, which one
712 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
713 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
715 # whether to show a usage summary line (total usage charges, no details)
716 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
717 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
720 # create lines for setup and (non-usage) recur, in the main section
721 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
722 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
724 # display everything in a single line
725 push @display, new FS::cust_bill_pkg_display
728 # and if usage_mandate is enabled, hide details
729 # (this only works on multisection invoices...)
730 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
734 if ($separate && $usage_section && $summary) {
735 # create a line for the usage summary in the main section
736 push @display, new FS::cust_bill_pkg_display { type => 'U',
742 if ($usage_mandate || ($usage_section && $summary) ) {
743 $hash{post_total} = 'Y';
746 if ($separate || $usage_mandate) {
747 # show call details for this line item in the usage section.
748 # if usage_mandate is on, this will display below the section subtotal.
749 # this also happens if usage is in a separate section and there's a
750 # summary in the main section, though I'm not sure why.
751 $hash{section} = $usage_section if $usage_section;
752 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
755 $self->set('display', \@display);
761 Returns a hash: keys are "setup", "recur" or usage classnum, values are
762 FS::cust_bill_pkg objects, each with no more than a single class (setup or
769 # XXX this goes away with cust_bill_pkg refactor
771 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
772 my %cust_bill_pkg = ();
774 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
775 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
778 #split setup and recur
779 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
780 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
781 $cust_bill_pkg->set('details', []);
782 $cust_bill_pkg->recur(0);
783 $cust_bill_pkg->unitrecur(0);
784 $cust_bill_pkg->type('');
785 $cust_bill_pkg_recur->setup(0);
786 $cust_bill_pkg_recur->unitsetup(0);
787 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
791 #split usage from recur
792 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
793 if exists($cust_bill_pkg{recur});
794 warn "usage is $usage\n" if $DEBUG > 1;
796 my $cust_bill_pkg_usage =
797 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
798 $cust_bill_pkg_usage->recur( $usage );
799 $cust_bill_pkg_usage->type( 'U' );
800 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
801 $cust_bill_pkg{recur}->recur( $recur );
802 $cust_bill_pkg{recur}->type( '' );
803 $cust_bill_pkg{recur}->set('details', []);
804 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
807 #subdivide usage by usage_class
808 if (exists($cust_bill_pkg{''})) {
809 foreach my $class (grep { $_ } $self->usage_classes) {
810 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
811 my $cust_bill_pkg_usage =
812 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
813 $cust_bill_pkg_usage->recur( $usage );
814 $cust_bill_pkg_usage->set('details', []);
815 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
816 $cust_bill_pkg{''}->recur( $classless );
817 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
819 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
820 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
821 delete $cust_bill_pkg{''}
822 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
825 # # sort setup,recur,'', and the rest numeric && return
826 # my @result = map { $cust_bill_pkg{$_} }
827 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
828 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
830 # keys %cust_bill_pkg;
839 Returns the amount of the charge associated with usage class CLASSNUM if
840 CLASSNUM is defined. Otherwise returns the total charge associated with
846 my( $self, $classnum ) = @_;
847 $self->regularize_details;
849 if ( $self->get('details') ) {
852 map { $_->amount || 0 }
853 grep { !defined($classnum) or $classnum eq $_->classnum }
854 @{ $self->get('details') }
859 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
860 ' WHERE billpkgnum = '. $self->billpkgnum;
861 $sql .= " AND classnum = $classnum" if defined($classnum);
863 my $sth = dbh->prepare($sql) or die dbh->errstr;
864 $sth->execute or die $sth->errstr;
866 return $sth->fetchrow_arrayref->[0] || 0;
874 Returns a list of usage classnums associated with this invoice line's
881 $self->regularize_details;
883 if ( $self->get('details') ) {
885 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
891 qsearch({ table => 'cust_bill_pkg_detail',
892 hashref => { billpkgnum => $self->billpkgnum },
893 select => 'DISTINCT classnum',
900 sub cust_tax_exempt_pkg {
903 $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
906 =item cust_bill_pkg_tax_Xlocation
908 Returns the list of associated cust_bill_pkg_tax_location and/or
909 cust_bill_pkg_tax_rate_location objects
913 sub cust_bill_pkg_tax_Xlocation {
916 my %hash = ( 'billpkgnum' => $self->billpkgnum );
919 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
920 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
925 =item recur_show_zero
929 sub recur_show_zero { shift->_X_show_zero('recur'); }
930 sub setup_show_zero { shift->_X_show_zero('setup'); }
933 my( $self, $what ) = @_;
935 return 0 unless $self->$what() == 0 && $self->pkgnum;
937 $self->cust_pkg->_X_show_zero($what);
940 =item credited [ BEFORE, AFTER, OPTIONS ]
942 Returns the sum of credits applied to this item. Arguments are the same as
943 owed_sql/paid_sql/credited_sql.
949 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
952 =item tax_locationnum
954 Returns the L<FS::cust_location> number that this line item is in for tax
955 purposes. For package sales, it's the package tax location; for fees,
956 it's the customer's default service location.
960 sub tax_locationnum {
962 if ( $self->pkgnum ) { # normal sales
963 return $self->cust_pkg->tax_locationnum;
964 } elsif ( $self->feepart ) { # fees
965 return $self->cust_bill->cust_main->ship_locationnum;
973 if ( $self->pkgnum ) { # normal sales
974 return $self->cust_pkg->tax_location;
975 } elsif ( $self->feepart ) { # fees
976 return $self->cust_bill->cust_main->ship_location;
984 Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
985 charge. If called on a tax line, returns nothing.
991 if ( $self->pkgpart_override ) {
992 return FS::part_pkg->by_key($self->pkgpart_override);
993 } elsif ( $self->pkgnum ) {
994 return $self->cust_pkg->part_pkg;
995 } elsif ( $self->feepart ) {
996 return $self->part_fee;
1004 =head1 CLASS METHODS
1010 Returns an SQL expression for the total usage charges in details on
1016 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1017 FROM cust_bill_pkg_detail
1018 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1020 sub usage_sql { $usage_sql }
1022 # this makes owed_sql, etc. much more concise
1024 my ($class, $start, $end, %opt) = @_;
1025 my $setuprecur = $opt{setuprecur} || '';
1027 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1028 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1029 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1031 if ($opt{no_usage} and $charged =~ /recur/) {
1032 $charged = "$charged - $usage_sql"
1039 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1041 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1042 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1043 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1049 '(' . $class->charged_sql(@_) .
1050 ' - ' . $class->paid_sql(@_) .
1051 ' - ' . $class->credited_sql(@_) . ')'
1054 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1056 Returns an SQL expression for the sum of payments applied to this item.
1061 my ($class, $start, $end, %opt) = @_;
1062 my $s = $start ? "AND cust_pay._date <= $start" : '';
1063 my $e = $end ? "AND cust_pay._date > $end" : '';
1064 my $setuprecur = $opt{setuprecur} || '';
1065 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1066 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1067 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1069 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1070 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1071 JOIN cust_pay USING (paynum)
1072 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1073 $s $e $setuprecur )";
1075 if ( $opt{no_usage} ) {
1076 # cap the amount paid at the sum of non-usage charges,
1077 # minus the amount credited against non-usage charges
1079 $class->charged_sql($start, $end, %opt) . ' - ' .
1080 $class->credited_sql($start, $end, %opt).')';
1089 my ($class, $start, $end, %opt) = @_;
1090 my $s = $start ? "AND cust_credit._date <= $start" : '';
1091 my $e = $end ? "AND cust_credit._date > $end" : '';
1092 my $setuprecur = $opt{setuprecur} || '';
1093 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1094 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1095 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1097 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1098 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1099 JOIN cust_credit USING (crednum)
1100 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1101 $s $e $setuprecur )";
1103 if ( $opt{no_usage} ) {
1104 # cap the amount credited at the sum of non-usage charges
1105 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1113 sub upgrade_tax_location {
1114 # For taxes that were calculated/invoiced before cust_location refactoring
1115 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1116 # they were calculated on a package-location basis. Create them here,
1117 # along with any necessary cust_location records and any tax exemption
1120 my ($class, %opt) = @_;
1121 # %opt may include 's' and 'e': start and end date ranges
1122 # and 'X': abort on any error, instead of just rolling back changes to
1125 my $oldAutoCommit = $FS::UID::AutoCommit;
1126 local $FS::UID::AutoCommit = 0;
1129 use FS::h_cust_main;
1130 use FS::h_cust_bill;
1132 use FS::h_cust_main_exemption;
1135 local $FS::cust_location::import = 1;
1137 my $conf = FS::Conf->new; # h_conf?
1138 return if $conf->exists('enable_taxproducts'); #don't touch this case
1139 my $use_ship = $conf->exists('tax-ship_address');
1141 my $date_where = '';
1143 $date_where .= " AND cust_bill._date >= $opt{s}";
1146 $date_where .= " AND cust_bill._date < $opt{e}";
1149 my $commit_each_invoice = 1 unless $opt{X};
1151 # if an invoice has either of these kinds of objects, then it doesn't
1152 # need to be upgraded...probably
1153 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1154 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1155 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1156 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1157 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1158 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1159 ' AND exempt_monthly IS NULL';
1161 my @invnums = map { $_->invnum } qsearch({
1162 select => 'cust_bill.invnum',
1163 table => 'cust_bill',
1165 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1166 "AND NOT EXISTS($sub_has_exempt) ".
1170 print "Processing ".scalar(@invnums)." invoices...\n";
1174 foreach my $invnum (@invnums) {
1176 print STDERR "Invoice #$invnum\n";
1178 my %pkgpart_taxclass; # pkgpart => taxclass
1179 my %pkgpart_exempt_setup;
1180 my %pkgpart_exempt_recur;
1181 my $h_cust_bill = qsearchs('h_cust_bill',
1182 { invnum => $invnum,
1183 history_action => 'insert' });
1184 if (!$h_cust_bill) {
1185 warn "no insert record for invoice $invnum; skipped\n";
1186 #$date = $cust_bill->_date as a fallback?
1187 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1188 # when looking up history records in other tables.
1191 my $custnum = $h_cust_bill->custnum;
1193 # Determine the address corresponding to this tax region.
1194 # It's either the bill or ship address of the customer as of the
1195 # invoice date-of-insertion. (Not necessarily the invoice date.)
1196 my $date = $h_cust_bill->history_date;
1197 my $h_cust_main = qsearchs('h_cust_main',
1198 { custnum => $custnum },
1199 FS::h_cust_main->sql_h_searchs($date)
1201 if (!$h_cust_main ) {
1202 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1204 # fallback to current $cust_main? sounds dangerous.
1207 # This is a historical customer record, so it has a historical address.
1208 # If there's no cust_location matching this custnum and address (there
1209 # probably isn't), create one.
1210 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1211 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1212 FS::cust_main->location_fields;
1213 # not really needed for this, and often result in duplicate locations
1214 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1216 $hash{custnum} = $h_cust_main->custnum;
1217 my $tax_loc = FS::cust_location->new(\%hash);
1218 my $error = $tax_loc->find_or_insert || $tax_loc->disable_if_unused;
1220 warn "couldn't create historical location record for cust#".
1221 $h_cust_main->custnum.": $error\n";
1224 my $exempt_cust = 1 if $h_cust_main->tax;
1226 # Get any per-customer taxname exemptions that were in effect.
1227 my %exempt_cust_taxname = map {
1229 } qsearch('h_cust_main_exemption', { 'custnum' => $custnum },
1230 FS::h_cust_main_exemption->sql_h_searchs($date)
1233 # classify line items
1235 my %nontax_items; # taxclass => array of cust_bill_pkg
1236 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1237 my $pkgnum = $item->pkgnum;
1239 if ( $pkgnum == 0 ) {
1241 push @tax_items, $item;
1244 # (pkgparts really shouldn't change, right?)
1245 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1246 FS::h_cust_pkg->sql_h_searchs($date)
1248 if ( !$h_cust_pkg ) {
1249 warn "no historical package #".$item->pkgpart."; skipped\n";
1252 my $pkgpart = $h_cust_pkg->pkgpart;
1254 if (!exists $pkgpart_taxclass{$pkgpart}) {
1255 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1256 FS::h_part_pkg->sql_h_searchs($date)
1258 if ( !$h_part_pkg ) {
1259 warn "no historical package def #$pkgpart; skipped\n";
1262 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1263 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1264 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1267 # mark any exemptions that apply
1268 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1269 $item->set('exempt_setup' => 1);
1272 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1273 $item->set('exempt_recur' => 1);
1276 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1278 $nontax_items{$taxclass} ||= [];
1279 push @{ $nontax_items{$taxclass} }, $item;
1282 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1285 # Use a variation on the procedure in
1286 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1288 my @loc_keys = qw( district city county state country );
1289 my %taxhash = map { $_ => $h_cust_main->get($pre.$_) } @loc_keys;
1290 my %taxdef_by_name; # by name, and then by taxclass
1291 my %est_tax; # by name, and then by taxclass
1292 my %taxable_items; # by taxnum, and then an array
1294 foreach my $taxclass (keys %nontax_items) {
1295 my %myhash = %taxhash;
1296 my @elim = qw( district city county state );
1297 my @taxdefs; # because there may be several with different taxnames
1299 $myhash{taxclass} = $taxclass;
1300 @taxdefs = qsearch('cust_main_county', \%myhash);
1302 $myhash{taxclass} = '';
1303 @taxdefs = qsearch('cust_main_county', \%myhash);
1305 $myhash{ shift @elim } = '';
1306 } while scalar(@elim) and !@taxdefs;
1308 print "Class '$taxclass': ". scalar(@{ $nontax_items{$taxclass} }).
1309 " items, ". scalar(@taxdefs)." tax defs found.\n";
1310 foreach my $taxdef (@taxdefs) {
1311 next if $taxdef->tax == 0;
1312 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1314 $taxable_items{$taxdef->taxnum} ||= [];
1315 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1316 # clone the item so that taxdef-dependent changes don't
1317 # change it for other taxdefs
1318 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1320 # these flags are already set if the part_pkg declares itself exempt
1321 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1322 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1325 my $taxable = $item->setup + $item->recur;
1327 # h_cust_credit_bill_pkg?
1328 # NO. Because if these exemptions HAD been created at the time of
1329 # billing, and then a credit applied later, the exemption would
1330 # have been adjusted by the amount of the credit. So we adjust
1331 # the taxable amount before creating the exemption.
1332 # But don't deduct the credit from taxable, because the tax was
1333 # calculated before the credit was applied.
1334 foreach my $f (qw(setup recur)) {
1335 my $credited = FS::Record->scalar_sql(
1336 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1337 "WHERE billpkgnum = ? AND setuprecur = ?",
1341 $item->set($f, $item->get($f) - $credited) if $credited;
1343 my $existing_exempt = FS::Record->scalar_sql(
1344 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1345 "billpkgnum = ? AND taxnum = ?",
1346 $item->billpkgnum, $taxdef->taxnum
1348 $taxable -= $existing_exempt;
1350 if ( $taxable and $exempt_cust ) {
1351 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1354 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1355 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1358 if ( $taxable and $item->exempt_setup ) {
1359 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1360 $taxable -= $item->setup;
1362 if ( $taxable and $item->exempt_recur ) {
1363 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1364 $taxable -= $item->recur;
1367 $item->set('taxable' => $taxable);
1368 push @{ $taxable_items{$taxdef->taxnum} }, $item
1371 # estimate the amount of tax (this is necessary because different
1372 # taxdefs with the same taxname may have different tax rates)
1373 # and sum that for each taxname/taxclass combination
1375 $est_tax{$taxdef->taxname} ||= {};
1376 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1377 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1378 $taxable * $taxdef->tax;
1380 foreach (@new_exempt) {
1381 next if $_->{amount} == 0;
1382 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1384 billpkgnum => $item->billpkgnum,
1385 taxnum => $taxdef->taxnum,
1387 my $error = $cust_tax_exempt_pkg->insert;
1389 my $pkgnum = $item->pkgnum;
1390 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1394 } #foreach @new_exempt
1397 } #foreach $taxclass
1399 # Now go through the billed taxes and match them up with the line items.
1400 TAX_ITEM: foreach my $tax_item ( @tax_items )
1402 my $taxname = $tax_item->itemdesc;
1403 $taxname = '' if $taxname eq 'Tax';
1405 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1406 # then we didn't find any applicable taxes with this name
1407 warn "no definition found for tax item '$taxname'.\n".
1408 '('.join(' ', @hash{qw(country state county city district)}).")\n";
1409 # possibly all of these should be "next TAX_ITEM", but whole invoices
1410 # are transaction protected and we can go back and retry them.
1413 # classname => cust_main_county
1414 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1416 # Divide the tax item among taxclasses, if necessary
1417 # classname => estimated tax amount
1418 my $this_est_tax = $est_tax{$taxname};
1419 if (!defined $this_est_tax) {
1420 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1423 my $est_total = sum(values %$this_est_tax);
1424 if ( $est_total == 0 ) {
1426 warn "estimated tax on invoice #$invnum is zero.\n";
1430 my $real_tax = $tax_item->setup;
1431 printf ("Distributing \$%.2f tax:\n", $real_tax);
1432 my $cents_remaining = $real_tax * 100; # for rounding error
1433 my @tax_links; # partial CBPTL hashrefs
1434 foreach my $taxclass (keys %taxdef_by_class) {
1435 my $taxdef = $taxdef_by_class{$taxclass};
1436 # these items already have "taxable" set to their charge amount
1437 # after applying any credits or exemptions
1438 my @items = @{ $taxable_items{$taxdef->taxnum} };
1439 my $subtotal = sum(map {$_->get('taxable')} @items);
1440 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1442 foreach my $nontax (@items) {
1443 my $part = int($real_tax
1445 * ($this_est_tax->{$taxclass}/$est_total)
1447 * ($nontax->get('taxable'))/$subtotal
1451 $cents_remaining -= $part;
1453 taxnum => $taxdef->taxnum,
1454 pkgnum => $nontax->pkgnum,
1455 billpkgnum => $nontax->billpkgnum,
1459 } #foreach $taxclass
1460 # Distribute any leftover tax round-robin style, one cent at a time.
1462 my $nlinks = scalar(@tax_links);
1464 while (int($cents_remaining) > 0) {
1465 $tax_links[$i % $nlinks]->{cents} += 1;
1470 warn "Can't create tax links--no taxable items found.\n";
1474 # Gather credit/payment applications so that we can link them
1477 qsearch( 'cust_credit_bill_pkg',
1478 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1480 qsearch( 'cust_bill_pay_pkg',
1481 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1485 # grab the first one
1486 my $this_unlinked = shift @unlinked;
1487 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1489 # Create tax links (yay!)
1490 printf("Creating %d tax links.\n",scalar(@tax_links));
1491 foreach (@tax_links) {
1492 my $link = FS::cust_bill_pkg_tax_location->new({
1493 billpkgnum => $tax_item->billpkgnum,
1494 taxtype => 'FS::cust_main_county',
1495 locationnum => $tax_loc->locationnum,
1496 taxnum => $_->{taxnum},
1497 pkgnum => $_->{pkgnum},
1498 amount => sprintf('%.2f', $_->{cents} / 100),
1499 taxable_billpkgnum => $_->{billpkgnum},
1501 my $error = $link->insert;
1503 warn "Can't create tax link for inv#$invnum: $error\n";
1507 my $link_cents = $_->{cents};
1508 # update/create subitem links
1510 # If $this_unlinked is undef, then we've allocated all of the
1511 # credit/payment applications to the tax item. If $link_cents is 0,
1512 # then we've applied credits/payments to all of this package fraction,
1513 # so go on to the next.
1514 while ($this_unlinked and $link_cents) {
1515 # apply as much as possible of $link_amount to this credit/payment
1517 my $apply_cents = min($link_cents, $unlinked_cents);
1518 $link_cents -= $apply_cents;
1519 $unlinked_cents -= $apply_cents;
1520 # $link_cents or $unlinked_cents or both are now zero
1521 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1522 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1523 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1524 if ( $this_unlinked->$pkey ) {
1525 # then it's an existing link--replace it
1526 $error = $this_unlinked->replace;
1528 $this_unlinked->insert;
1530 # what do we do with errors at this stage?
1532 warn "Error creating tax application link: $error\n";
1533 next INVOICE; # for lack of a better idea
1536 if ( $unlinked_cents == 0 ) {
1537 # then we've allocated all of this payment/credit application,
1538 # so grab the next one
1539 $this_unlinked = shift @unlinked;
1540 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1541 } elsif ( $link_cents == 0 ) {
1542 # then we've covered all of this package tax fraction, so split
1543 # off a new application from this one
1544 $this_unlinked = $this_unlinked->new({
1545 $this_unlinked->hash,
1548 # $unlinked_cents is still what it is
1551 } #while $this_unlinked and $link_cents
1552 } #foreach (@tax_links)
1553 } #foreach $tax_item
1555 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1561 $dbh->rollback if $oldAutoCommit;
1562 die "Upgrade halted.\n" unless $commit_each_invoice;
1566 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1571 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1575 use Date::Parse 'str2time';
1578 my $upgrade = 'tax_location_2012';
1579 return if FS::upgrade_journal->is_done($upgrade);
1580 my $job = FS::queue->new({
1581 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1583 # call it kind of like a class method, not that it matters much
1584 $job->insert($class, 's' => str2time('2012-01-01'));
1585 # if there's a customer location upgrade queued also, wait for it to
1587 my $location_job = qsearchs('queue', {
1588 job => 'FS::cust_main::Location::process_upgrade_location'
1590 if ( $location_job ) {
1591 $job->depend_insert($location_job->jobnum);
1593 # Then mark the upgrade as done, so that we don't queue the job twice
1594 # and somehow run two of them concurrently.
1595 FS::upgrade_journal->set_done($upgrade);
1596 # This upgrade now does the job of assigning taxable_billpkgnums to
1597 # cust_bill_pkg_tax_location, so set that task done also.
1598 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1605 setup and recur shouldn't be separate fields. There should be one "amount"
1606 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1608 A line item with both should really be two separate records (preserving
1609 sdate and edate for setup fees for recurring packages - that information may
1610 be valuable later). Invoice generation (cust_main::bill), invoice printing
1611 (cust_bill), tax reports (report_tax.cgi) and line item reports
1612 (cust_bill_pkg.cgi) would need to be updated.
1614 owed_setup and owed_recur could then be repaced by just owed, and
1615 cust_bill::open_cust_bill_pkg and
1616 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1618 The upgrade procedure is pretty sketchy.
1622 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1623 from the base documentation.