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;
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_moneyn('unitsetup')
485 || $self->ut_currencyn('setup_billed_currency')
486 || $self->ut_moneyn('setup_billed_amount')
487 || $self->ut_money('recur')
488 || $self->ut_moneyn('unitrecur')
489 || $self->ut_currencyn('recur_billed_currency')
490 || $self->ut_moneyn('recur_billed_amount')
491 || $self->ut_numbern('sdate')
492 || $self->ut_numbern('edate')
493 || $self->ut_textn('itemdesc')
494 || $self->ut_textn('itemcomment')
495 || $self->ut_enum('hidden', [ '', 'Y' ])
497 return $error if $error;
499 $self->regularize_details;
501 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
502 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
503 return "Unknown pkgnum ". $self->pkgnum
504 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
507 return "Unknown invnum"
508 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
513 =item regularize_details
515 Converts the contents of the 'details' pseudo-field to
516 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
520 sub regularize_details {
522 if ( $self->get('details') ) {
523 foreach my $detail ( @{$self->get('details')} ) {
524 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
525 # then turn it into one
527 if ( ! ref($detail) ) {
528 $hash{'detail'} = $detail;
530 elsif ( ref($detail) eq 'HASH' ) {
533 elsif ( ref($detail) eq 'ARRAY' ) {
534 carp "passing invoice details as arrays is deprecated";
535 #carp "this way sucks, use a hash"; #but more useful/friendly
536 $hash{'format'} = $detail->[0];
537 $hash{'detail'} = $detail->[1];
538 $hash{'amount'} = $detail->[2];
539 $hash{'classnum'} = $detail->[3];
540 $hash{'phonenum'} = $detail->[4];
541 $hash{'accountcode'} = $detail->[5];
542 $hash{'startdate'} = $detail->[6];
543 $hash{'duration'} = $detail->[7];
544 $hash{'regionname'} = $detail->[8];
547 die "unknown detail type ". ref($detail);
549 $detail = new FS::cust_bill_pkg_detail \%hash;
551 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
559 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
563 Returns the customer (L<FS::cust_main> object) for this line item.
568 # required for cust_main_Mixin equivalence
569 # and use cust_bill instead of cust_pkg because this might not have a
572 my $cust_bill = $self->cust_bill or return '';
573 $cust_bill->cust_main;
576 =item previous_cust_bill_pkg
578 Returns the previous cust_bill_pkg for this package, if any.
582 sub previous_cust_bill_pkg {
584 return unless $self->sdate;
586 'table' => 'cust_bill_pkg',
587 'hashref' => { 'pkgnum' => $self->pkgnum,
588 'sdate' => { op=>'<', value=>$self->sdate },
590 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
596 Returns the amount owed (still outstanding) on this line item's setup fee,
597 which is the amount of the line item minus all payment applications (see
598 L<FS::cust_bill_pay_pkg> and credit applications (see
599 L<FS::cust_credit_bill_pkg>).
605 $self->owed('setup', @_);
610 Returns the amount owed (still outstanding) on this line item's recurring fee,
611 which is the amount of the line item minus all payment applications (see
612 L<FS::cust_bill_pay_pkg> and credit applications (see
613 L<FS::cust_credit_bill_pkg>).
619 $self->owed('recur', @_);
622 # modeled after cust_bill::owed...
624 my( $self, $field ) = @_;
625 my $balance = $self->$field();
626 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
627 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
628 $balance = sprintf( '%.2f', $balance );
629 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
635 my( $self, $field ) = @_;
636 my $balance = $self->$field();
637 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
638 $balance = sprintf( '%.2f', $balance );
639 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
643 sub cust_bill_pay_pkg {
644 my( $self, $field ) = @_;
645 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
646 'setuprecur' => $field,
651 sub cust_credit_bill_pkg {
652 my( $self, $field ) = @_;
653 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
654 'setuprecur' => $field,
661 Returns the number of billing units (for tax purposes) represented by this,
668 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
673 If this item has any discounts, returns a hashref in the format used
674 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
675 on an invoice. This will contain the keys 'description', 'amount',
676 'ext_description' (an arrayref of text lines describing the discounts),
677 and '_is_discount' (a flag).
679 The value for 'amount' will be negative, and will be scaled for the package
686 my @pkg_discounts = $self->pkg_discount;
687 return if @pkg_discounts == 0;
688 # special case: if there are old "discount details" on this line item, don't
689 # show discount line items
690 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
697 description => $self->mt('Discount'),
699 ext_description => \@ext,
700 # maybe should show quantity/unit discount?
702 foreach my $pkg_discount (@pkg_discounts) {
703 push @ext, $pkg_discount->description;
704 $d->{amount} -= $pkg_discount->amount;
706 $d->{amount} *= $self->quantity || 1;
711 =item set_display OPTION => VALUE ...
713 A helper method for I<insert>, populates the pseudo-field B<display> with
714 appropriate FS::cust_bill_pkg_display objects.
716 Options are passed as a list of name/value pairs. Options are:
718 part_pkg: FS::part_pkg object from this line item's package.
720 real_pkgpart: if this line item comes from a bundled package, the pkgpart
721 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
726 my( $self, %opt ) = @_;
727 my $part_pkg = $opt{'part_pkg'};
728 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
730 my $conf = new FS::Conf;
732 # whether to break this down into setup/recur/usage
733 my $separate = $conf->exists('separate_usage');
735 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
736 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
738 # or use the category from $opt{'part_pkg'} if its not bundled?
739 my $categoryname = $cust_pkg->part_pkg->categoryname;
741 # if we don't have to separate setup/recur/usage, or put this in a
742 # package-specific section, or display a usage summary, then don't
743 # even create one of these. The item will just display in the unnamed
744 # section as a single line plus details.
745 return $self->set('display', [])
746 unless $separate || $categoryname || $usage_mandate;
750 my %hash = ( 'section' => $categoryname );
752 # whether to put usage details in a separate section, and if so, which one
753 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
754 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
756 # whether to show a usage summary line (total usage charges, no details)
757 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
758 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
761 # create lines for setup and (non-usage) recur, in the main section
762 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
763 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
765 # display everything in a single line
766 push @display, new FS::cust_bill_pkg_display
769 # and if usage_mandate is enabled, hide details
770 # (this only works on multisection invoices...)
771 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
775 if ($separate && $usage_section && $summary) {
776 # create a line for the usage summary in the main section
777 push @display, new FS::cust_bill_pkg_display { type => 'U',
783 if ($usage_mandate || ($usage_section && $summary) ) {
784 $hash{post_total} = 'Y';
787 if ($separate || $usage_mandate) {
788 # show call details for this line item in the usage section.
789 # if usage_mandate is on, this will display below the section subtotal.
790 # this also happens if usage is in a separate section and there's a
791 # summary in the main section, though I'm not sure why.
792 $hash{section} = $usage_section if $usage_section;
793 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
796 $self->set('display', \@display);
802 Returns a hash: keys are "setup", "recur" or usage classnum, values are
803 FS::cust_bill_pkg objects, each with no more than a single class (setup or
810 # XXX this goes away with cust_bill_pkg refactor
812 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
813 my %cust_bill_pkg = ();
815 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
816 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
819 #split setup and recur
820 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
821 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
822 $cust_bill_pkg->set('details', []);
823 $cust_bill_pkg->recur(0);
824 $cust_bill_pkg->unitrecur(0);
825 $cust_bill_pkg->type('');
826 $cust_bill_pkg_recur->setup(0);
827 $cust_bill_pkg_recur->unitsetup(0);
828 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
832 #split usage from recur
833 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
834 if exists($cust_bill_pkg{recur});
835 warn "usage is $usage\n" if $DEBUG > 1;
837 my $cust_bill_pkg_usage =
838 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
839 $cust_bill_pkg_usage->recur( $usage );
840 $cust_bill_pkg_usage->type( 'U' );
841 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
842 $cust_bill_pkg{recur}->recur( $recur );
843 $cust_bill_pkg{recur}->type( '' );
844 $cust_bill_pkg{recur}->set('details', []);
845 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
848 #subdivide usage by usage_class
849 if (exists($cust_bill_pkg{''})) {
850 foreach my $class (grep { $_ } $self->usage_classes) {
851 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
852 my $cust_bill_pkg_usage =
853 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
854 $cust_bill_pkg_usage->recur( $usage );
855 $cust_bill_pkg_usage->set('details', []);
856 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
857 $cust_bill_pkg{''}->recur( $classless );
858 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
860 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
861 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
862 delete $cust_bill_pkg{''}
863 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
866 # # sort setup,recur,'', and the rest numeric && return
867 # my @result = map { $cust_bill_pkg{$_} }
868 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
869 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
871 # keys %cust_bill_pkg;
880 Returns the amount of the charge associated with usage class CLASSNUM if
881 CLASSNUM is defined. Otherwise returns the total charge associated with
887 my( $self, $classnum ) = @_;
888 $self->regularize_details;
890 if ( $self->get('details') ) {
893 map { $_->amount || 0 }
894 grep { !defined($classnum) or $classnum eq $_->classnum }
895 @{ $self->get('details') }
900 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
901 ' WHERE billpkgnum = '. $self->billpkgnum;
902 if (defined $classnum) {
903 if ($classnum =~ /^(\d+)$/) {
904 $sql .= " AND classnum = $1";
905 } elsif ($classnum eq '') {
906 $sql .= " AND classnum IS NULL";
910 my $sth = dbh->prepare($sql) or die dbh->errstr;
911 $sth->execute or die $sth->errstr;
913 return $sth->fetchrow_arrayref->[0] || 0;
921 Returns a list of usage classnums associated with this invoice line's
928 $self->regularize_details;
930 if ( $self->get('details') ) {
932 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
938 qsearch({ table => 'cust_bill_pkg_detail',
939 hashref => { billpkgnum => $self->billpkgnum },
940 select => 'DISTINCT classnum',
947 sub cust_tax_exempt_pkg {
950 $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
953 =item cust_bill_pkg_tax_Xlocation
955 Returns the list of associated cust_bill_pkg_tax_location and/or
956 cust_bill_pkg_tax_rate_location objects
960 sub cust_bill_pkg_tax_Xlocation {
963 my %hash = ( 'billpkgnum' => $self->billpkgnum );
966 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
967 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
972 =item recur_show_zero
976 sub recur_show_zero { shift->_X_show_zero('recur'); }
977 sub setup_show_zero { shift->_X_show_zero('setup'); }
980 my( $self, $what ) = @_;
982 return 0 unless $self->$what() == 0 && $self->pkgnum;
984 $self->cust_pkg->_X_show_zero($what);
987 =item credited [ BEFORE, AFTER, OPTIONS ]
989 Returns the sum of credits applied to this item. Arguments are the same as
990 owed_sql/paid_sql/credited_sql.
996 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
999 =item tax_locationnum
1001 Returns the L<FS::cust_location> number that this line item is in for tax
1002 purposes. For package sales, it's the package tax location; for fees,
1003 it's the customer's default service location.
1007 sub tax_locationnum {
1009 if ( $self->pkgnum ) { # normal sales
1010 return $self->cust_pkg->tax_locationnum;
1011 } elsif ( $self->feepart ) { # fees
1012 return $self->cust_bill->cust_main->ship_locationnum;
1020 if ( $self->pkgnum ) { # normal sales
1021 return $self->cust_pkg->tax_location;
1022 } elsif ( $self->feepart ) { # fees
1023 return $self->cust_bill->cust_main->ship_location;
1031 Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
1032 charge. If called on a tax line, returns nothing.
1038 if ( $self->pkgpart_override ) {
1039 return FS::part_pkg->by_key($self->pkgpart_override);
1040 } elsif ( $self->pkgnum ) {
1041 return $self->cust_pkg->part_pkg;
1042 } elsif ( $self->feepart ) {
1043 return $self->part_fee;
1051 =head1 CLASS METHODS
1057 Returns an SQL expression for the total usage charges in details on
1063 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1064 FROM cust_bill_pkg_detail
1065 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1067 sub usage_sql { $usage_sql }
1069 # this makes owed_sql, etc. much more concise
1071 my ($class, $start, $end, %opt) = @_;
1072 my $setuprecur = $opt{setuprecur} || '';
1074 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1075 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1076 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1078 if ($opt{no_usage} and $charged =~ /recur/) {
1079 $charged = "$charged - $usage_sql"
1086 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1088 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1089 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1090 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1096 '(' . $class->charged_sql(@_) .
1097 ' - ' . $class->paid_sql(@_) .
1098 ' - ' . $class->credited_sql(@_) . ')'
1101 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1103 Returns an SQL expression for the sum of payments applied to this item.
1108 my ($class, $start, $end, %opt) = @_;
1109 my $s = $start ? "AND cust_pay._date <= $start" : '';
1110 my $e = $end ? "AND cust_pay._date > $end" : '';
1111 my $setuprecur = $opt{setuprecur} || '';
1112 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1113 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1114 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1116 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1117 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1118 JOIN cust_pay USING (paynum)
1119 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1120 $s $e $setuprecur )";
1122 if ( $opt{no_usage} ) {
1123 # cap the amount paid at the sum of non-usage charges,
1124 # minus the amount credited against non-usage charges
1126 $class->charged_sql($start, $end, %opt) . ' - ' .
1127 $class->credited_sql($start, $end, %opt).')';
1136 my ($class, $start, $end, %opt) = @_;
1137 my $s = $start ? "AND cust_credit._date <= $start" : '';
1138 my $e = $end ? "AND cust_credit._date > $end" : '';
1139 my $setuprecur = $opt{setuprecur} || '';
1140 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1141 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1142 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1144 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1145 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1146 JOIN cust_credit USING (crednum)
1147 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1148 $s $e $setuprecur )";
1150 if ( $opt{no_usage} ) {
1151 # cap the amount credited at the sum of non-usage charges
1152 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1160 sub upgrade_tax_location {
1161 # For taxes that were calculated/invoiced before cust_location refactoring
1162 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1163 # they were calculated on a package-location basis. Create them here,
1164 # along with any necessary cust_location records and any tax exemption
1167 my ($class, %opt) = @_;
1168 # %opt may include 's' and 'e': start and end date ranges
1169 # and 'X': abort on any error, instead of just rolling back changes to
1172 my $oldAutoCommit = $FS::UID::AutoCommit;
1173 local $FS::UID::AutoCommit = 0;
1176 use FS::h_cust_main;
1177 use FS::h_cust_bill;
1179 use FS::h_cust_main_exemption;
1182 local $FS::cust_location::import = 1;
1184 my $conf = FS::Conf->new; # h_conf?
1185 return if $conf->exists('enable_taxproducts'); #don't touch this case
1186 my $use_ship = $conf->exists('tax-ship_address');
1187 my $use_pkgloc = $conf->exists('tax-pkg_address');
1189 my $date_where = '';
1191 $date_where .= " AND cust_bill._date >= $opt{s}";
1194 $date_where .= " AND cust_bill._date < $opt{e}";
1197 my $commit_each_invoice = 1 unless $opt{X};
1199 # if an invoice has either of these kinds of objects, then it doesn't
1200 # need to be upgraded...probably
1201 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1202 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1203 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1204 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1205 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1206 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1207 ' AND exempt_monthly IS NULL';
1209 my %all_tax_names = (
1212 map { $_->taxname => 1 }
1213 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1216 my $search = FS::Cursor->new({
1217 table => 'cust_bill',
1219 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1220 "AND NOT EXISTS($sub_has_exempt) ".
1224 #print "Processing ".scalar(@invnums)." invoices...\n";
1228 while (my $cust_bill = $search->fetch) {
1229 my $invnum = $cust_bill->invnum;
1231 print STDERR "Invoice #$invnum\n";
1233 my %pkgpart_taxclass; # pkgpart => taxclass
1234 my %pkgpart_exempt_setup;
1235 my %pkgpart_exempt_recur;
1236 my $h_cust_bill = qsearchs('h_cust_bill',
1237 { invnum => $invnum,
1238 history_action => 'insert' });
1239 if (!$h_cust_bill) {
1240 warn "no insert record for invoice $invnum; skipped\n";
1241 #$date = $cust_bill->_date as a fallback?
1242 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1243 # when looking up history records in other tables.
1246 my $custnum = $h_cust_bill->custnum;
1248 # Determine the address corresponding to this tax region.
1249 # It's either the bill or ship address of the customer as of the
1250 # invoice date-of-insertion. (Not necessarily the invoice date.)
1251 my $date = $h_cust_bill->history_date;
1252 local($FS::Record::qsearch_qualify_columns) = 0;
1253 my $h_cust_main = qsearchs('h_cust_main',
1254 { custnum => $custnum },
1255 FS::h_cust_main->sql_h_searchs($date)
1257 if (!$h_cust_main ) {
1258 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1260 # fallback to current $cust_main? sounds dangerous.
1263 # This is a historical customer record, so it has a historical address.
1264 # If there's no cust_location matching this custnum and address (there
1265 # probably isn't), create one.
1266 my %tax_loc; # keys are pkgnums, values are cust_location objects
1267 my $default_tax_loc;
1268 if ( $h_cust_main->bill_locationnum ) {
1269 # the location has already been upgraded
1271 $default_tax_loc = $h_cust_main->ship_location;
1273 $default_tax_loc = $h_cust_main->bill_location;
1276 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1277 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1278 FS::cust_main->location_fields;
1279 # not really needed for this, and often result in duplicate locations
1280 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1282 $hash{custnum} = $h_cust_main->custnum;
1283 $default_tax_loc = FS::cust_location->new(\%hash);
1284 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1286 warn "couldn't create historical location record for cust#".
1287 $h_cust_main->custnum.": $error\n";
1292 $exempt_cust = 1 if $h_cust_main->tax;
1294 # classify line items
1296 my %nontax_items; # taxclass => array of cust_bill_pkg
1297 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1298 my $pkgnum = $item->pkgnum;
1300 if ( $pkgnum == 0 ) {
1302 push @tax_items, $item;
1305 # (pkgparts really shouldn't change, right?)
1306 local($FS::Record::qsearch_qualify_columns) = 0;
1307 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1308 FS::h_cust_pkg->sql_h_searchs($date)
1310 if ( !$h_cust_pkg ) {
1311 warn "no historical package #".$item->pkgpart."; skipped\n";
1314 my $pkgpart = $h_cust_pkg->pkgpart;
1316 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1317 # then this package already had a locationnum assigned, and that's
1318 # the one to use for tax calculation
1319 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1321 # use the customer's bill or ship loc, which was inserted earlier
1322 $tax_loc{$pkgnum} = $default_tax_loc;
1325 if (!exists $pkgpart_taxclass{$pkgpart}) {
1326 local($FS::Record::qsearch_qualify_columns) = 0;
1327 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1328 FS::h_part_pkg->sql_h_searchs($date)
1330 if ( !$h_part_pkg ) {
1331 warn "no historical package def #$pkgpart; skipped\n";
1334 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1335 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1336 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1339 # mark any exemptions that apply
1340 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1341 $item->set('exempt_setup' => 1);
1344 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1345 $item->set('exempt_recur' => 1);
1348 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1350 $nontax_items{$taxclass} ||= [];
1351 push @{ $nontax_items{$taxclass} }, $item;
1355 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1358 # Get any per-customer taxname exemptions that were in effect.
1359 my %exempt_cust_taxname;
1360 foreach (keys %all_tax_names) {
1361 local($FS::Record::qsearch_qualify_columns) = 0;
1362 my $h_exemption = qsearchs('h_cust_main_exemption', {
1363 'custnum' => $custnum,
1366 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1369 $exempt_cust_taxname{ $_ } = 1;
1373 # Use a variation on the procedure in
1374 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1376 my @loc_keys = qw( district city county state country );
1377 my %taxdef_by_name; # by name, and then by taxclass
1378 my %est_tax; # by name, and then by taxclass
1379 my %taxable_items; # by taxnum, and then an array
1381 foreach my $taxclass (keys %nontax_items) {
1382 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1383 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1384 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1385 my @elim = qw( district city county state );
1386 my @taxdefs; # because there may be several with different taxnames
1388 $myhash{taxclass} = $taxclass;
1389 @taxdefs = qsearch('cust_main_county', \%myhash);
1391 $myhash{taxclass} = '';
1392 @taxdefs = qsearch('cust_main_county', \%myhash);
1394 $myhash{ shift @elim } = '';
1395 } while scalar(@elim) and !@taxdefs;
1397 foreach my $taxdef (@taxdefs) {
1398 next if $taxdef->tax == 0;
1399 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1401 $taxable_items{$taxdef->taxnum} ||= [];
1402 # clone the item so that taxdef-dependent changes don't
1403 # change it for other taxdefs
1404 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1406 # these flags are already set if the part_pkg declares itself exempt
1407 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1408 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1411 my $taxable = $item->setup + $item->recur;
1413 # h_cust_credit_bill_pkg?
1414 # NO. Because if these exemptions HAD been created at the time of
1415 # billing, and then a credit applied later, the exemption would
1416 # have been adjusted by the amount of the credit. So we adjust
1417 # the taxable amount before creating the exemption.
1418 # But don't deduct the credit from taxable, because the tax was
1419 # calculated before the credit was applied.
1420 foreach my $f (qw(setup recur)) {
1421 my $credited = FS::Record->scalar_sql(
1422 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1423 "WHERE billpkgnum = ? AND setuprecur = ?",
1427 $item->set($f, $item->get($f) - $credited) if $credited;
1429 my $existing_exempt = FS::Record->scalar_sql(
1430 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1431 "billpkgnum = ? AND taxnum = ?",
1432 $item->billpkgnum, $taxdef->taxnum
1434 $taxable -= $existing_exempt;
1436 if ( $taxable and $exempt_cust ) {
1437 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1440 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1441 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1444 if ( $taxable and $item->exempt_setup ) {
1445 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1446 $taxable -= $item->setup;
1448 if ( $taxable and $item->exempt_recur ) {
1449 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1450 $taxable -= $item->recur;
1453 $item->set('taxable' => $taxable);
1454 push @{ $taxable_items{$taxdef->taxnum} }, $item
1457 # estimate the amount of tax (this is necessary because different
1458 # taxdefs with the same taxname may have different tax rates)
1459 # and sum that for each taxname/taxclass combination
1461 $est_tax{$taxdef->taxname} ||= {};
1462 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1463 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1464 $taxable * $taxdef->tax;
1466 foreach (@new_exempt) {
1467 next if $_->{amount} == 0;
1468 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1470 billpkgnum => $item->billpkgnum,
1471 taxnum => $taxdef->taxnum,
1473 my $error = $cust_tax_exempt_pkg->insert;
1475 my $pkgnum = $item->pkgnum;
1476 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1480 } #foreach @new_exempt
1483 } #foreach $taxclass
1485 # Now go through the billed taxes and match them up with the line items.
1486 TAX_ITEM: foreach my $tax_item ( @tax_items )
1488 my $taxname = $tax_item->itemdesc;
1489 $taxname = '' if $taxname eq 'Tax';
1491 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1492 # then we didn't find any applicable taxes with this name
1493 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1494 # possibly all of these should be "next TAX_ITEM", but whole invoices
1495 # are transaction protected and we can go back and retry them.
1498 # classname => cust_main_county
1499 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1501 # Divide the tax item among taxclasses, if necessary
1502 # classname => estimated tax amount
1503 my $this_est_tax = $est_tax{$taxname};
1504 if (!defined $this_est_tax) {
1505 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1508 my $est_total = sum(values %$this_est_tax);
1509 if ( $est_total == 0 ) {
1511 warn "estimated tax on invoice #$invnum is zero.\n";
1515 my $real_tax = $tax_item->setup;
1516 printf ("Distributing \$%.2f tax:\n", $real_tax);
1517 my $cents_remaining = $real_tax * 100; # for rounding error
1518 my @tax_links; # partial CBPTL hashrefs
1519 foreach my $taxclass (keys %taxdef_by_class) {
1520 my $taxdef = $taxdef_by_class{$taxclass};
1521 # these items already have "taxable" set to their charge amount
1522 # after applying any credits or exemptions
1523 my @items = @{ $taxable_items{$taxdef->taxnum} };
1524 my $subtotal = sum(map {$_->get('taxable')} @items);
1525 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1527 foreach my $nontax (@items) {
1528 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1529 my $part = int($real_tax
1531 * ($this_est_tax->{$taxclass}/$est_total)
1533 * ($nontax->get('taxable'))/$subtotal
1537 $cents_remaining -= $part;
1539 taxnum => $taxdef->taxnum,
1540 pkgnum => $nontax->pkgnum,
1541 locationnum => $my_tax_loc->locationnum,
1542 billpkgnum => $nontax->billpkgnum,
1546 } #foreach $taxclass
1547 # Distribute any leftover tax round-robin style, one cent at a time.
1549 my $nlinks = scalar(@tax_links);
1551 # ensure that it really is an integer
1552 $cents_remaining = sprintf('%.0f', $cents_remaining);
1553 while ($cents_remaining > 0) {
1554 $tax_links[$i % $nlinks]->{cents} += 1;
1559 warn "Can't create tax links--no taxable items found.\n";
1563 # Gather credit/payment applications so that we can link them
1566 qsearch( 'cust_credit_bill_pkg',
1567 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1569 qsearch( 'cust_bill_pay_pkg',
1570 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1574 # grab the first one
1575 my $this_unlinked = shift @unlinked;
1576 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1578 # Create tax links (yay!)
1579 printf("Creating %d tax links.\n",scalar(@tax_links));
1580 foreach (@tax_links) {
1581 my $link = FS::cust_bill_pkg_tax_location->new({
1582 billpkgnum => $tax_item->billpkgnum,
1583 taxtype => 'FS::cust_main_county',
1584 locationnum => $_->{locationnum},
1585 taxnum => $_->{taxnum},
1586 pkgnum => $_->{pkgnum},
1587 amount => sprintf('%.2f', $_->{cents} / 100),
1588 taxable_billpkgnum => $_->{billpkgnum},
1590 my $error = $link->insert;
1592 warn "Can't create tax link for inv#$invnum: $error\n";
1596 my $link_cents = $_->{cents};
1597 # update/create subitem links
1599 # If $this_unlinked is undef, then we've allocated all of the
1600 # credit/payment applications to the tax item. If $link_cents is 0,
1601 # then we've applied credits/payments to all of this package fraction,
1602 # so go on to the next.
1603 while ($this_unlinked and $link_cents) {
1604 # apply as much as possible of $link_amount to this credit/payment
1606 my $apply_cents = min($link_cents, $unlinked_cents);
1607 $link_cents -= $apply_cents;
1608 $unlinked_cents -= $apply_cents;
1609 # $link_cents or $unlinked_cents or both are now zero
1610 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1611 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1612 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1613 if ( $this_unlinked->$pkey ) {
1614 # then it's an existing link--replace it
1615 $error = $this_unlinked->replace;
1617 $this_unlinked->insert;
1619 # what do we do with errors at this stage?
1621 warn "Error creating tax application link: $error\n";
1622 next INVOICE; # for lack of a better idea
1625 if ( $unlinked_cents == 0 ) {
1626 # then we've allocated all of this payment/credit application,
1627 # so grab the next one
1628 $this_unlinked = shift @unlinked;
1629 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1630 } elsif ( $link_cents == 0 ) {
1631 # then we've covered all of this package tax fraction, so split
1632 # off a new application from this one
1633 $this_unlinked = $this_unlinked->new({
1634 $this_unlinked->hash,
1637 # $unlinked_cents is still what it is
1640 } #while $this_unlinked and $link_cents
1641 } #foreach (@tax_links)
1642 } #foreach $tax_item
1644 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1650 $dbh->rollback if $oldAutoCommit;
1651 die "Upgrade halted.\n" unless $commit_each_invoice;
1655 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1660 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1664 use Date::Parse 'str2time';
1667 my $upgrade = 'tax_location_2012';
1668 return if FS::upgrade_journal->is_done($upgrade);
1669 my $job = FS::queue->new({
1670 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1672 # call it kind of like a class method, not that it matters much
1673 $job->insert($class, 's' => str2time('2012-01-01'));
1674 # if there's a customer location upgrade queued also, wait for it to
1676 my $location_job = qsearchs('queue', {
1677 job => 'FS::cust_main::Location::process_upgrade_location'
1679 if ( $location_job ) {
1680 $job->depend_insert($location_job->jobnum);
1682 # Then mark the upgrade as done, so that we don't queue the job twice
1683 # and somehow run two of them concurrently.
1684 FS::upgrade_journal->set_done($upgrade);
1685 # This upgrade now does the job of assigning taxable_billpkgnums to
1686 # cust_bill_pkg_tax_location, so set that task done also.
1687 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1694 setup and recur shouldn't be separate fields. There should be one "amount"
1695 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1697 A line item with both should really be two separate records (preserving
1698 sdate and edate for setup fees for recurring packages - that information may
1699 be valuable later). Invoice generation (cust_main::bill), invoice printing
1700 (cust_bill), tax reports (report_tax.cgi) and line item reports
1701 (cust_bill_pkg.cgi) would need to be updated.
1703 owed_setup and owed_recur could then be repaced by just owed, and
1704 cust_bill::open_cust_bill_pkg and
1705 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1707 The upgrade procedure is pretty sketchy.
1711 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1712 from the base documentation.