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;
29 use FS::cust_bill_pkg_fee_void;
35 $me = '[FS::cust_bill_pkg]';
39 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
43 use FS::cust_bill_pkg;
45 $record = new FS::cust_bill_pkg \%hash;
46 $record = new FS::cust_bill_pkg { 'column' => 'value' };
48 $error = $record->insert;
50 $error = $record->check;
54 An FS::cust_bill_pkg object represents an invoice line item.
55 FS::cust_bill_pkg inherits from FS::Record. The following fields are
66 invoice (see L<FS::cust_bill>)
70 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)
72 =item pkgpart_override
74 optional package definition (see L<FS::part_pkg>) override
86 starting date of recurring fee
90 ending date of recurring fee
94 Line item description (overrides normal package description)
98 If not set, defaults to 1
102 If not set, defaults to setup
106 If not set, defaults to recur
110 If set to Y, indicates data should not appear as separate line item on invoice
114 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
115 see L<Time::Local> and L<Date::Parse> for conversion functions.
123 Creates a new line item. To add the line item to the database, see
124 L<"insert">. Line items are normally created by calling the bill method of a
125 customer object (see L<FS::cust_main>).
129 sub table { 'cust_bill_pkg'; }
131 sub detail_table { 'cust_bill_pkg_detail'; }
132 sub display_table { 'cust_bill_pkg_display'; }
133 sub discount_table { 'cust_bill_pkg_discount'; }
134 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
135 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
136 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
140 Adds this line item to the database. If there is an error, returns the error,
141 otherwise returns false.
148 local $SIG{HUP} = 'IGNORE';
149 local $SIG{INT} = 'IGNORE';
150 local $SIG{QUIT} = 'IGNORE';
151 local $SIG{TERM} = 'IGNORE';
152 local $SIG{TSTP} = 'IGNORE';
153 local $SIG{PIPE} = 'IGNORE';
155 my $oldAutoCommit = $FS::UID::AutoCommit;
156 local $FS::UID::AutoCommit = 0;
159 my $error = $self->SUPER::insert;
161 $dbh->rollback if $oldAutoCommit;
165 if ( $self->get('details') ) {
166 foreach my $detail ( @{$self->get('details')} ) {
167 $detail->billpkgnum($self->billpkgnum);
168 $error = $detail->insert;
170 $dbh->rollback if $oldAutoCommit;
171 return "error inserting cust_bill_pkg_detail: $error";
176 if ( $self->get('display') ) {
177 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
178 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
179 $error = $cust_bill_pkg_display->insert;
181 $dbh->rollback if $oldAutoCommit;
182 return "error inserting cust_bill_pkg_display: $error";
187 if ( $self->get('discounts') ) {
188 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
189 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
190 $error = $cust_bill_pkg_discount->insert;
192 $dbh->rollback if $oldAutoCommit;
193 return "error inserting cust_bill_pkg_discount: $error";
198 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
199 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
200 $error = $cust_tax_exempt_pkg->insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "error inserting cust_tax_exempt_pkg: $error";
207 foreach my $tax_link_table (qw(cust_bill_pkg_tax_location
208 cust_bill_pkg_tax_rate_location))
210 my $tax_location = $self->get($tax_link_table) || [];
211 foreach my $link ( @$tax_location ) {
212 my $pkey = $link->primary_key;
213 next if $link->get($pkey); # don't try to double-insert
214 # This cust_bill_pkg can be linked on either side (i.e. it can be the
215 # tax or the taxed item). If the other side is already inserted,
216 # then set billpkgnum to ours, and insert the link. Otherwise,
217 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
218 # on the other side, to be inserted later.
220 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
221 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
222 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
223 # break circular links when doing this
224 $link->set('tax_cust_bill_pkg', '');
226 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
227 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
228 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
229 # XXX pkgnum is zero for tax on tax; it might be better to use
230 # the underlying package?
231 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
232 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
233 $link->set('taxable_cust_bill_pkg', '');
236 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
237 $error = $link->insert;
239 $dbh->rollback if $oldAutoCommit;
240 return "error inserting cust_bill_pkg_tax_location: $error";
244 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
245 : $link->get('tax_cust_bill_pkg');
246 my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
247 push @$link_array, $link;
248 $other->set('cust_bill_pkg_tax_location' => $link_array);
253 # someday you will be as awesome as cust_bill_pkg_tax_location...
254 # and today is that day
255 #my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
256 #if ( $tax_rate_location ) {
257 # foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
258 # $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
259 # $error = $cust_bill_pkg_tax_rate_location->insert;
261 # $dbh->rollback if $oldAutoCommit;
262 # return "error inserting cust_bill_pkg_tax_rate_location: $error";
267 my $fee_links = $self->get('cust_bill_pkg_fee');
269 foreach my $link ( @$fee_links ) {
270 # very similar to cust_bill_pkg_tax_location, for obvious reasons
271 next if $link->billpkgfeenum; # don't try to double-insert
273 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
274 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
276 if ( $target and $target->billpkgnum ) {
277 $link->set('billpkgnum', $target->billpkgnum);
278 # base_invnum => null indicates that the fee is based on its own
280 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
281 $link->set('cust_bill_pkg', '');
284 if ( $base and $base->billpkgnum ) {
285 $link->set('base_billpkgnum', $base->billpkgnum);
286 $link->set('base_cust_bill_pkg', '');
288 # it's based on a line item that's not yet inserted
289 my $link_array = $base->get('cust_bill_pkg_fee') || [];
290 push @$link_array, $link;
291 $base->set('cust_bill_pkg_fee' => $link_array);
292 next; # don't insert the link yet
295 $error = $link->insert;
297 $dbh->rollback if $oldAutoCommit;
298 return "error inserting cust_bill_pkg_fee: $error";
303 my $cust_event_fee = $self->get('cust_event_fee');
304 if ( $cust_event_fee ) {
305 $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
306 $error = $cust_event_fee->replace;
308 $dbh->rollback if $oldAutoCommit;
309 return "error updating cust_event_fee: $error";
313 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
314 if ( $cust_tax_adjustment ) {
315 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
316 $error = $cust_tax_adjustment->replace;
318 $dbh->rollback if $oldAutoCommit;
319 return "error replacing cust_tax_adjustment: $error";
323 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
330 Voids this line item: deletes the line item and adds a record of the voided
331 line item to the FS::cust_bill_pkg_void table (and related tables).
337 my $reason = scalar(@_) ? shift : '';
339 local $SIG{HUP} = 'IGNORE';
340 local $SIG{INT} = 'IGNORE';
341 local $SIG{QUIT} = 'IGNORE';
342 local $SIG{TERM} = 'IGNORE';
343 local $SIG{TSTP} = 'IGNORE';
344 local $SIG{PIPE} = 'IGNORE';
346 my $oldAutoCommit = $FS::UID::AutoCommit;
347 local $FS::UID::AutoCommit = 0;
350 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
351 map { $_ => $self->get($_) } $self->fields
353 $cust_bill_pkg_void->reason($reason);
354 my $error = $cust_bill_pkg_void->insert;
356 $dbh->rollback if $oldAutoCommit;
360 foreach my $table (qw(
362 cust_bill_pkg_display
363 cust_bill_pkg_discount
364 cust_bill_pkg_tax_location
365 cust_bill_pkg_tax_rate_location
370 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
372 my $vclass = 'FS::'.$table.'_void';
373 my $void = $vclass->new( {
374 map { $_ => $linked->get($_) } $linked->fields
376 my $error = $void->insert || $linked->delete;
378 $dbh->rollback if $oldAutoCommit;
386 $error = $self->delete;
388 $dbh->rollback if $oldAutoCommit;
392 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
407 local $SIG{HUP} = 'IGNORE';
408 local $SIG{INT} = 'IGNORE';
409 local $SIG{QUIT} = 'IGNORE';
410 local $SIG{TERM} = 'IGNORE';
411 local $SIG{TSTP} = 'IGNORE';
412 local $SIG{PIPE} = 'IGNORE';
414 my $oldAutoCommit = $FS::UID::AutoCommit;
415 local $FS::UID::AutoCommit = 0;
418 foreach my $table (qw(
420 cust_bill_pkg_display
421 cust_bill_pkg_discount
422 cust_bill_pkg_tax_location
423 cust_bill_pkg_tax_rate_location
430 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
431 my $error = $linked->delete;
433 $dbh->rollback if $oldAutoCommit;
440 foreach my $cust_tax_adjustment (
441 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
443 $cust_tax_adjustment->billpkgnum(''); #NULL
444 my $error = $cust_tax_adjustment->replace;
446 $dbh->rollback if $oldAutoCommit;
451 my $error = $self->SUPER::delete(@_);
453 $dbh->rollback if $oldAutoCommit;
457 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
463 #alas, bin/follow-tax-rename
465 #=item replace OLD_RECORD
467 #Currently unimplemented. This would be even more of an accounting nightmare
468 #than deleteing the items. Just don't do it.
473 # return "Can't modify cust_bill_pkg records!";
478 Checks all fields to make sure this is a valid line item. If there is an
479 error, returns the error, otherwise returns false. Called by the insert
488 $self->ut_numbern('billpkgnum')
489 || $self->ut_snumber('pkgnum')
490 || $self->ut_number('invnum')
491 || $self->ut_money('setup')
492 || $self->ut_money('recur')
493 || $self->ut_numbern('sdate')
494 || $self->ut_numbern('edate')
495 || $self->ut_textn('itemdesc')
496 || $self->ut_textn('itemcomment')
497 || $self->ut_enum('hidden', [ '', 'Y' ])
499 return $error if $error;
501 $self->regularize_details;
503 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
504 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
505 return "Unknown pkgnum ". $self->pkgnum
506 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
509 return "Unknown invnum"
510 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
515 =item regularize_details
517 Converts the contents of the 'details' pseudo-field to
518 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
522 sub regularize_details {
524 if ( $self->get('details') ) {
525 foreach my $detail ( @{$self->get('details')} ) {
526 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
527 # then turn it into one
529 if ( ! ref($detail) ) {
530 $hash{'detail'} = $detail;
532 elsif ( ref($detail) eq 'HASH' ) {
535 elsif ( ref($detail) eq 'ARRAY' ) {
536 carp "passing invoice details as arrays is deprecated";
537 #carp "this way sucks, use a hash"; #but more useful/friendly
538 $hash{'format'} = $detail->[0];
539 $hash{'detail'} = $detail->[1];
540 $hash{'amount'} = $detail->[2];
541 $hash{'classnum'} = $detail->[3];
542 $hash{'phonenum'} = $detail->[4];
543 $hash{'accountcode'} = $detail->[5];
544 $hash{'startdate'} = $detail->[6];
545 $hash{'duration'} = $detail->[7];
546 $hash{'regionname'} = $detail->[8];
549 die "unknown detail type ". ref($detail);
551 $detail = new FS::cust_bill_pkg_detail \%hash;
553 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
559 =item set_exemptions TAXOBJECT, OPTIONS
561 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
562 L<FS::tax_rate> record for the tax.
564 This will deal with the following cases:
568 =item Fully exempt customers (cust_main.tax flag) or customer classes
571 =item Customers exempt from specific named taxes (cust_main_exemption
574 =item Taxes that don't apply to setup or recurring fees
575 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
577 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
580 =item Fees that aren't marked as taxable (part_fee.taxable).
584 It does NOT deal with monthly tax exemptions, which need more context
585 than this humble little method cares to deal with.
587 OPTIONS should include "custnum" => the customer number if this tax line
588 hasn't been inserted (which it probably hasn't).
590 Returns a list of exemption objects, which will also be attached to the
591 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
592 item will insert these records as well.
601 my $part_pkg = $self->part_pkg;
602 my $part_fee = $self->part_fee;
605 my $custnum = $opt{custnum};
606 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
608 $cust_main = FS::cust_main->by_key( $custnum )
609 or die "set_exemptions can't identify customer (pass custnum option)\n";
612 my $taxable_charged = $self->setup + $self->recur;
613 return unless $taxable_charged > 0;
615 ### Fully exempt customer ###
617 my $conf = FS::Conf->new;
618 if ( $conf->exists('cust_class-tax_exempt') ) {
619 my $cust_class = $cust_main->cust_class;
620 $exempt_cust = $cust_class->tax if $cust_class;
622 $exempt_cust = $cust_main->tax;
625 ### Exemption from named tax ###
626 my $exempt_cust_taxname;
627 if ( !$exempt_cust and $tax->taxname ) {
628 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
631 if ( $exempt_cust ) {
633 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
634 amount => $taxable_charged,
637 $taxable_charged = 0;
639 } elsif ( $exempt_cust_taxname ) {
641 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
642 amount => $taxable_charged,
643 exempt_cust_taxname => 'Y',
645 $taxable_charged = 0;
649 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
650 or ($part_pkg and $part_pkg->setuptax)
655 and $taxable_charged > 0 ) {
657 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
658 amount => $self->setup,
661 $taxable_charged -= $self->setup;
665 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
666 or ($part_pkg and $part_pkg->recurtax)
671 and $taxable_charged > 0 ) {
673 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
674 amount => $self->recur,
677 $taxable_charged -= $self->recur;
681 foreach (@new_exemptions) {
682 $_->set('taxnum', $tax->taxnum);
683 $_->set('taxtype', ref($tax));
686 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
687 return @new_exemptions;
693 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
699 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
704 Returns the customer (L<FS::cust_main> object) for this line item.
709 # required for cust_main_Mixin equivalence
710 # and use cust_bill instead of cust_pkg because this might not have a
713 my $cust_bill = $self->cust_bill or return '';
714 $cust_bill->cust_main;
717 =item previous_cust_bill_pkg
719 Returns the previous cust_bill_pkg for this package, if any.
723 sub previous_cust_bill_pkg {
725 return unless $self->sdate;
727 'table' => 'cust_bill_pkg',
728 'hashref' => { 'pkgnum' => $self->pkgnum,
729 'sdate' => { op=>'<', value=>$self->sdate },
731 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
737 Returns the amount owed (still outstanding) on this line item's setup fee,
738 which is the amount of the line item minus all payment applications (see
739 L<FS::cust_bill_pay_pkg> and credit applications (see
740 L<FS::cust_credit_bill_pkg>).
746 $self->owed('setup', @_);
751 Returns the amount owed (still outstanding) on this line item's recurring fee,
752 which is the amount of the line item minus all payment applications (see
753 L<FS::cust_bill_pay_pkg> and credit applications (see
754 L<FS::cust_credit_bill_pkg>).
760 $self->owed('recur', @_);
763 # modeled after cust_bill::owed...
765 my( $self, $field ) = @_;
766 my $balance = $self->$field();
767 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
768 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
769 $balance = sprintf( '%.2f', $balance );
770 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
776 my( $self, $field ) = @_;
777 my $balance = $self->$field();
778 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
779 $balance = sprintf( '%.2f', $balance );
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
784 sub cust_bill_pay_pkg {
785 my( $self, $field ) = @_;
786 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
787 'setuprecur' => $field,
792 sub cust_credit_bill_pkg {
793 my( $self, $field ) = @_;
794 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
795 'setuprecur' => $field,
802 Returns the number of billing units (for tax purposes) represented by this,
809 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
814 If this item has any discounts, returns a hashref in the format used
815 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
816 on an invoice. This will contain the keys 'description', 'amount',
817 'ext_description' (an arrayref of text lines describing the discounts),
818 and '_is_discount' (a flag).
820 The value for 'amount' will be negative, and will be scaled for the package
827 my @pkg_discounts = $self->pkg_discount;
828 return if @pkg_discounts == 0;
829 # special case: if there are old "discount details" on this line item, don't
830 # show discount line items
831 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
838 description => $self->mt('Discount'),
840 ext_description => \@ext,
841 # maybe should show quantity/unit discount?
843 foreach my $pkg_discount (@pkg_discounts) {
844 push @ext, $pkg_discount->description;
845 $d->{amount} -= $pkg_discount->amount;
847 $d->{amount} *= $self->quantity || 1;
852 =item set_display OPTION => VALUE ...
854 A helper method for I<insert>, populates the pseudo-field B<display> with
855 appropriate FS::cust_bill_pkg_display objects.
857 Options are passed as a list of name/value pairs. Options are:
859 part_pkg: FS::part_pkg object from this line item's package.
861 real_pkgpart: if this line item comes from a bundled package, the pkgpart
862 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
867 my( $self, %opt ) = @_;
868 my $part_pkg = $opt{'part_pkg'};
869 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
871 my $conf = new FS::Conf;
873 # whether to break this down into setup/recur/usage
874 my $separate = $conf->exists('separate_usage');
876 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
877 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
879 # or use the category from $opt{'part_pkg'} if its not bundled?
880 my $categoryname = $cust_pkg->part_pkg->categoryname;
882 # if we don't have to separate setup/recur/usage, or put this in a
883 # package-specific section, or display a usage summary, then don't
884 # even create one of these. The item will just display in the unnamed
885 # section as a single line plus details.
886 return $self->set('display', [])
887 unless $separate || $categoryname || $usage_mandate;
891 my %hash = ( 'section' => $categoryname );
893 # whether to put usage details in a separate section, and if so, which one
894 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
895 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
897 # whether to show a usage summary line (total usage charges, no details)
898 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
899 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
902 # create lines for setup and (non-usage) recur, in the main section
903 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
904 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
906 # display everything in a single line
907 push @display, new FS::cust_bill_pkg_display
910 # and if usage_mandate is enabled, hide details
911 # (this only works on multisection invoices...)
912 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
916 if ($separate && $usage_section && $summary) {
917 # create a line for the usage summary in the main section
918 push @display, new FS::cust_bill_pkg_display { type => 'U',
924 if ($usage_mandate || ($usage_section && $summary) ) {
925 $hash{post_total} = 'Y';
928 if ($separate || $usage_mandate) {
929 # show call details for this line item in the usage section.
930 # if usage_mandate is on, this will display below the section subtotal.
931 # this also happens if usage is in a separate section and there's a
932 # summary in the main section, though I'm not sure why.
933 $hash{section} = $usage_section if $usage_section;
934 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
937 $self->set('display', \@display);
943 Returns a hash: keys are "setup", "recur" or usage classnum, values are
944 FS::cust_bill_pkg objects, each with no more than a single class (setup or
951 # XXX this goes away with cust_bill_pkg refactor
952 # or at least I wish it would, but it turns out to be harder than
955 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
956 my %cust_bill_pkg = ();
959 foreach my $classnum ($self->usage_classes) {
960 next if $classnum eq ''; # null-class usage is included in 'recur'
961 my $amount = $self->usage($classnum);
962 next if $amount == 0; # though if so we shouldn't be here
963 my $usage_item = FS::cust_bill_pkg->new({
967 'taxclass' => $classnum,
970 $cust_bill_pkg{$classnum} = $usage_item;
971 $usage_total += $amount;
974 foreach (qw(setup recur)) {
975 next if ($self->get($_) == 0);
976 my $item = FS::cust_bill_pkg->new({
983 $item->set($_, $self->get($_));
984 $cust_bill_pkg{$_} = $item;
988 $cust_bill_pkg{recur}->set('recur',
989 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
998 Returns the amount of the charge associated with usage class CLASSNUM if
999 CLASSNUM is defined. Otherwise returns the total charge associated with
1005 my( $self, $classnum ) = @_;
1006 $self->regularize_details;
1008 if ( $self->get('details') ) {
1011 map { $_->amount || 0 }
1012 grep { !defined($classnum) or $classnum eq $_->classnum }
1013 @{ $self->get('details') }
1018 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1019 ' WHERE billpkgnum = '. $self->billpkgnum;
1020 if (defined $classnum) {
1021 if ($classnum =~ /^(\d+)$/) {
1022 $sql .= " AND classnum = $1";
1023 } elsif (defined($classnum) and $classnum eq '') {
1024 $sql .= " AND classnum IS NULL";
1028 my $sth = dbh->prepare($sql) or die dbh->errstr;
1029 $sth->execute or die $sth->errstr;
1031 return $sth->fetchrow_arrayref->[0] || 0;
1039 Returns a list of usage classnums associated with this invoice line's
1046 $self->regularize_details;
1048 if ( $self->get('details') ) {
1050 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1055 map { $_->classnum }
1056 qsearch({ table => 'cust_bill_pkg_detail',
1057 hashref => { billpkgnum => $self->billpkgnum },
1058 select => 'DISTINCT classnum',
1065 sub cust_tax_exempt_pkg {
1068 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1071 =item cust_bill_pkg_fee
1073 Returns the list of associated cust_bill_pkg_fee objects, if this is
1078 sub cust_bill_pkg_fee {
1080 qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
1083 =item cust_bill_pkg_tax_Xlocation
1085 Returns the list of associated cust_bill_pkg_tax_location and/or
1086 cust_bill_pkg_tax_rate_location objects
1090 sub cust_bill_pkg_tax_Xlocation {
1093 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1096 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1097 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1102 =item recur_show_zero
1106 sub recur_show_zero { shift->_X_show_zero('recur'); }
1107 sub setup_show_zero { shift->_X_show_zero('setup'); }
1110 my( $self, $what ) = @_;
1112 return 0 unless $self->$what() == 0 && $self->pkgnum;
1114 $self->cust_pkg->_X_show_zero($what);
1117 =item credited [ BEFORE, AFTER, OPTIONS ]
1119 Returns the sum of credits applied to this item. Arguments are the same as
1120 owed_sql/paid_sql/credited_sql.
1126 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1129 =item tax_locationnum
1131 Returns the L<FS::cust_location> number that this line item is in for tax
1132 purposes. For package sales, it's the package tax location; for fees,
1133 it's the customer's default service location.
1137 sub tax_locationnum {
1139 if ( $self->pkgnum ) { # normal sales
1140 return $self->cust_pkg->tax_locationnum;
1141 } elsif ( $self->feepart and $self->invnum ) { # fees
1142 return $self->cust_bill->cust_main->ship_locationnum;
1150 if ( $self->pkgnum ) { # normal sales
1151 return $self->cust_pkg->tax_location;
1152 } elsif ( $self->feepart and $self->invnum ) { # fees
1153 return $self->cust_bill->cust_main->ship_location;
1161 =head1 CLASS METHODS
1167 Returns an SQL expression for the total usage charges in details on
1173 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1174 FROM cust_bill_pkg_detail
1175 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1177 sub usage_sql { $usage_sql }
1179 # this makes owed_sql, etc. much more concise
1181 my ($class, $start, $end, %opt) = @_;
1182 my $setuprecur = $opt{setuprecur} || '';
1184 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1185 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1186 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1188 if ($opt{no_usage} and $charged =~ /recur/) {
1189 $charged = "$charged - $usage_sql"
1196 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1198 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1199 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1200 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1206 '(' . $class->charged_sql(@_) .
1207 ' - ' . $class->paid_sql(@_) .
1208 ' - ' . $class->credited_sql(@_) . ')'
1211 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1213 Returns an SQL expression for the sum of payments applied to this item.
1218 my ($class, $start, $end, %opt) = @_;
1219 my $s = $start ? "AND cust_pay._date <= $start" : '';
1220 my $e = $end ? "AND cust_pay._date > $end" : '';
1221 my $setuprecur = $opt{setuprecur} || '';
1222 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1223 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1224 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1226 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1227 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1228 JOIN cust_pay USING (paynum)
1229 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1230 $s $e $setuprecur )";
1232 if ( $opt{no_usage} ) {
1233 # cap the amount paid at the sum of non-usage charges,
1234 # minus the amount credited against non-usage charges
1236 $class->charged_sql($start, $end, %opt) . ' - ' .
1237 $class->credited_sql($start, $end, %opt).')';
1246 my ($class, $start, $end, %opt) = @_;
1247 my $s = $start ? "AND cust_credit._date <= $start" : '';
1248 my $e = $end ? "AND cust_credit._date > $end" : '';
1249 my $setuprecur = $opt{setuprecur} || '';
1250 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1251 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1252 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1254 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1255 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1256 JOIN cust_credit USING (crednum)
1257 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1258 $s $e $setuprecur )";
1260 if ( $opt{no_usage} ) {
1261 # cap the amount credited at the sum of non-usage charges
1262 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1270 sub upgrade_tax_location {
1271 # For taxes that were calculated/invoiced before cust_location refactoring
1272 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1273 # they were calculated on a package-location basis. Create them here,
1274 # along with any necessary cust_location records and any tax exemption
1277 my ($class, %opt) = @_;
1278 # %opt may include 's' and 'e': start and end date ranges
1279 # and 'X': abort on any error, instead of just rolling back changes to
1282 my $oldAutoCommit = $FS::UID::AutoCommit;
1283 local $FS::UID::AutoCommit = 0;
1286 use FS::h_cust_main;
1287 use FS::h_cust_bill;
1289 use FS::h_cust_main_exemption;
1292 local $FS::cust_location::import = 1;
1294 my $conf = FS::Conf->new; # h_conf?
1295 return if $conf->exists('enable_taxproducts'); #don't touch this case
1296 my $use_ship = $conf->exists('tax-ship_address');
1297 my $use_pkgloc = $conf->exists('tax-pkg_address');
1299 my $date_where = '';
1301 $date_where .= " AND cust_bill._date >= $opt{s}";
1304 $date_where .= " AND cust_bill._date < $opt{e}";
1307 my $commit_each_invoice = 1 unless $opt{X};
1309 # if an invoice has either of these kinds of objects, then it doesn't
1310 # need to be upgraded...probably
1311 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1312 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1313 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1314 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1315 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1316 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1317 ' AND exempt_monthly IS NULL';
1319 my %all_tax_names = (
1322 map { $_->taxname => 1 }
1323 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1326 my $search = FS::Cursor->new({
1327 table => 'cust_bill',
1329 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1330 "AND NOT EXISTS($sub_has_exempt) ".
1334 #print "Processing ".scalar(@invnums)." invoices...\n";
1338 while (my $cust_bill = $search->fetch) {
1339 my $invnum = $cust_bill->invnum;
1341 print STDERR "Invoice #$invnum\n";
1343 my %pkgpart_taxclass; # pkgpart => taxclass
1344 my %pkgpart_exempt_setup;
1345 my %pkgpart_exempt_recur;
1346 my $h_cust_bill = qsearchs('h_cust_bill',
1347 { invnum => $invnum,
1348 history_action => 'insert' });
1349 if (!$h_cust_bill) {
1350 warn "no insert record for invoice $invnum; skipped\n";
1351 #$date = $cust_bill->_date as a fallback?
1352 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1353 # when looking up history records in other tables.
1356 my $custnum = $h_cust_bill->custnum;
1358 # Determine the address corresponding to this tax region.
1359 # It's either the bill or ship address of the customer as of the
1360 # invoice date-of-insertion. (Not necessarily the invoice date.)
1361 my $date = $h_cust_bill->history_date;
1362 my $h_cust_main = qsearchs('h_cust_main',
1363 { custnum => $custnum },
1364 FS::h_cust_main->sql_h_searchs($date)
1366 if (!$h_cust_main ) {
1367 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1369 # fallback to current $cust_main? sounds dangerous.
1372 # This is a historical customer record, so it has a historical address.
1373 # If there's no cust_location matching this custnum and address (there
1374 # probably isn't), create one.
1375 my %tax_loc; # keys are pkgnums, values are cust_location objects
1376 my $default_tax_loc;
1377 if ( $h_cust_main->bill_locationnum ) {
1378 # the location has already been upgraded
1380 $default_tax_loc = $h_cust_main->ship_location;
1382 $default_tax_loc = $h_cust_main->bill_location;
1385 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1386 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1387 FS::cust_main->location_fields;
1388 # not really needed for this, and often result in duplicate locations
1389 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1391 $hash{custnum} = $h_cust_main->custnum;
1392 $default_tax_loc = FS::cust_location->new(\%hash);
1393 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1395 warn "couldn't create historical location record for cust#".
1396 $h_cust_main->custnum.": $error\n";
1401 $exempt_cust = 1 if $h_cust_main->tax;
1403 # classify line items
1405 my %nontax_items; # taxclass => array of cust_bill_pkg
1406 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1407 my $pkgnum = $item->pkgnum;
1409 if ( $pkgnum == 0 ) {
1411 push @tax_items, $item;
1414 # (pkgparts really shouldn't change, right?)
1415 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1416 FS::h_cust_pkg->sql_h_searchs($date)
1418 if ( !$h_cust_pkg ) {
1419 warn "no historical package #".$item->pkgpart."; skipped\n";
1422 my $pkgpart = $h_cust_pkg->pkgpart;
1424 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1425 # then this package already had a locationnum assigned, and that's
1426 # the one to use for tax calculation
1427 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1429 # use the customer's bill or ship loc, which was inserted earlier
1430 $tax_loc{$pkgnum} = $default_tax_loc;
1433 if (!exists $pkgpart_taxclass{$pkgpart}) {
1434 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1435 FS::h_part_pkg->sql_h_searchs($date)
1437 if ( !$h_part_pkg ) {
1438 warn "no historical package def #$pkgpart; skipped\n";
1441 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1442 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1443 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1446 # mark any exemptions that apply
1447 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1448 $item->set('exempt_setup' => 1);
1451 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1452 $item->set('exempt_recur' => 1);
1455 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1457 $nontax_items{$taxclass} ||= [];
1458 push @{ $nontax_items{$taxclass} }, $item;
1462 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1465 # Get any per-customer taxname exemptions that were in effect.
1466 my %exempt_cust_taxname;
1467 foreach (keys %all_tax_names) {
1468 my $h_exemption = qsearchs('h_cust_main_exemption', {
1469 'custnum' => $custnum,
1472 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1475 $exempt_cust_taxname{ $_ } = 1;
1479 # Use a variation on the procedure in
1480 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1482 my @loc_keys = qw( district city county state country );
1483 my %taxdef_by_name; # by name, and then by taxclass
1484 my %est_tax; # by name, and then by taxclass
1485 my %taxable_items; # by taxnum, and then an array
1487 foreach my $taxclass (keys %nontax_items) {
1488 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1489 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1490 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1491 my @elim = qw( district city county state );
1492 my @taxdefs; # because there may be several with different taxnames
1494 $myhash{taxclass} = $taxclass;
1495 @taxdefs = qsearch('cust_main_county', \%myhash);
1497 $myhash{taxclass} = '';
1498 @taxdefs = qsearch('cust_main_county', \%myhash);
1500 $myhash{ shift @elim } = '';
1501 } while scalar(@elim) and !@taxdefs;
1503 foreach my $taxdef (@taxdefs) {
1504 next if $taxdef->tax == 0;
1505 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1507 $taxable_items{$taxdef->taxnum} ||= [];
1508 # clone the item so that taxdef-dependent changes don't
1509 # change it for other taxdefs
1510 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1512 # these flags are already set if the part_pkg declares itself exempt
1513 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1514 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1517 my $taxable = $item->setup + $item->recur;
1519 # h_cust_credit_bill_pkg?
1520 # NO. Because if these exemptions HAD been created at the time of
1521 # billing, and then a credit applied later, the exemption would
1522 # have been adjusted by the amount of the credit. So we adjust
1523 # the taxable amount before creating the exemption.
1524 # But don't deduct the credit from taxable, because the tax was
1525 # calculated before the credit was applied.
1526 foreach my $f (qw(setup recur)) {
1527 my $credited = FS::Record->scalar_sql(
1528 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1529 "WHERE billpkgnum = ? AND setuprecur = ?",
1533 $item->set($f, $item->get($f) - $credited) if $credited;
1535 my $existing_exempt = FS::Record->scalar_sql(
1536 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1537 "billpkgnum = ? AND taxnum = ?",
1538 $item->billpkgnum, $taxdef->taxnum
1540 $taxable -= $existing_exempt;
1542 if ( $taxable and $exempt_cust ) {
1543 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1546 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1547 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1550 if ( $taxable and $item->exempt_setup ) {
1551 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1552 $taxable -= $item->setup;
1554 if ( $taxable and $item->exempt_recur ) {
1555 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1556 $taxable -= $item->recur;
1559 $item->set('taxable' => $taxable);
1560 push @{ $taxable_items{$taxdef->taxnum} }, $item
1563 # estimate the amount of tax (this is necessary because different
1564 # taxdefs with the same taxname may have different tax rates)
1565 # and sum that for each taxname/taxclass combination
1567 $est_tax{$taxdef->taxname} ||= {};
1568 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1569 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1570 $taxable * $taxdef->tax;
1572 foreach (@new_exempt) {
1573 next if $_->{amount} == 0;
1574 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1576 billpkgnum => $item->billpkgnum,
1577 taxnum => $taxdef->taxnum,
1579 my $error = $cust_tax_exempt_pkg->insert;
1581 my $pkgnum = $item->pkgnum;
1582 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1586 } #foreach @new_exempt
1589 } #foreach $taxclass
1591 # Now go through the billed taxes and match them up with the line items.
1592 TAX_ITEM: foreach my $tax_item ( @tax_items )
1594 my $taxname = $tax_item->itemdesc;
1595 $taxname = '' if $taxname eq 'Tax';
1597 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1598 # then we didn't find any applicable taxes with this name
1599 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1600 # possibly all of these should be "next TAX_ITEM", but whole invoices
1601 # are transaction protected and we can go back and retry them.
1604 # classname => cust_main_county
1605 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1607 # Divide the tax item among taxclasses, if necessary
1608 # classname => estimated tax amount
1609 my $this_est_tax = $est_tax{$taxname};
1610 if (!defined $this_est_tax) {
1611 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1614 my $est_total = sum(values %$this_est_tax);
1615 if ( $est_total == 0 ) {
1617 warn "estimated tax on invoice #$invnum is zero.\n";
1621 my $real_tax = $tax_item->setup;
1622 printf ("Distributing \$%.2f tax:\n", $real_tax);
1623 my $cents_remaining = $real_tax * 100; # for rounding error
1624 my @tax_links; # partial CBPTL hashrefs
1625 foreach my $taxclass (keys %taxdef_by_class) {
1626 my $taxdef = $taxdef_by_class{$taxclass};
1627 # these items already have "taxable" set to their charge amount
1628 # after applying any credits or exemptions
1629 my @items = @{ $taxable_items{$taxdef->taxnum} };
1630 my $subtotal = sum(map {$_->get('taxable')} @items);
1631 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1633 foreach my $nontax (@items) {
1634 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1635 my $part = int($real_tax
1637 * ($this_est_tax->{$taxclass}/$est_total)
1639 * ($nontax->get('taxable'))/$subtotal
1643 $cents_remaining -= $part;
1645 taxnum => $taxdef->taxnum,
1646 pkgnum => $nontax->pkgnum,
1647 locationnum => $my_tax_loc->locationnum,
1648 billpkgnum => $nontax->billpkgnum,
1652 } #foreach $taxclass
1653 # Distribute any leftover tax round-robin style, one cent at a time.
1655 my $nlinks = scalar(@tax_links);
1657 # ensure that it really is an integer
1658 $cents_remaining = sprintf('%.0f', $cents_remaining);
1659 while ($cents_remaining > 0) {
1660 $tax_links[$i % $nlinks]->{cents} += 1;
1665 warn "Can't create tax links--no taxable items found.\n";
1669 # Gather credit/payment applications so that we can link them
1672 qsearch( 'cust_credit_bill_pkg',
1673 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1675 qsearch( 'cust_bill_pay_pkg',
1676 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1680 # grab the first one
1681 my $this_unlinked = shift @unlinked;
1682 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1684 # Create tax links (yay!)
1685 printf("Creating %d tax links.\n",scalar(@tax_links));
1686 foreach (@tax_links) {
1687 my $link = FS::cust_bill_pkg_tax_location->new({
1688 billpkgnum => $tax_item->billpkgnum,
1689 taxtype => 'FS::cust_main_county',
1690 locationnum => $_->{locationnum},
1691 taxnum => $_->{taxnum},
1692 pkgnum => $_->{pkgnum},
1693 amount => sprintf('%.2f', $_->{cents} / 100),
1694 taxable_billpkgnum => $_->{billpkgnum},
1696 my $error = $link->insert;
1698 warn "Can't create tax link for inv#$invnum: $error\n";
1702 my $link_cents = $_->{cents};
1703 # update/create subitem links
1705 # If $this_unlinked is undef, then we've allocated all of the
1706 # credit/payment applications to the tax item. If $link_cents is 0,
1707 # then we've applied credits/payments to all of this package fraction,
1708 # so go on to the next.
1709 while ($this_unlinked and $link_cents) {
1710 # apply as much as possible of $link_amount to this credit/payment
1712 my $apply_cents = min($link_cents, $unlinked_cents);
1713 $link_cents -= $apply_cents;
1714 $unlinked_cents -= $apply_cents;
1715 # $link_cents or $unlinked_cents or both are now zero
1716 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1717 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1718 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1719 if ( $this_unlinked->$pkey ) {
1720 # then it's an existing link--replace it
1721 $error = $this_unlinked->replace;
1723 $this_unlinked->insert;
1725 # what do we do with errors at this stage?
1727 warn "Error creating tax application link: $error\n";
1728 next INVOICE; # for lack of a better idea
1731 if ( $unlinked_cents == 0 ) {
1732 # then we've allocated all of this payment/credit application,
1733 # so grab the next one
1734 $this_unlinked = shift @unlinked;
1735 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1736 } elsif ( $link_cents == 0 ) {
1737 # then we've covered all of this package tax fraction, so split
1738 # off a new application from this one
1739 $this_unlinked = $this_unlinked->new({
1740 $this_unlinked->hash,
1743 # $unlinked_cents is still what it is
1746 } #while $this_unlinked and $link_cents
1747 } #foreach (@tax_links)
1748 } #foreach $tax_item
1750 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1756 $dbh->rollback if $oldAutoCommit;
1757 die "Upgrade halted.\n" unless $commit_each_invoice;
1761 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1766 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1770 use Date::Parse 'str2time';
1773 my $upgrade = 'tax_location_2012';
1774 return if FS::upgrade_journal->is_done($upgrade);
1775 my $job = FS::queue->new({
1776 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1778 # call it kind of like a class method, not that it matters much
1779 $job->insert($class, 's' => str2time('2012-01-01'));
1780 # if there's a customer location upgrade queued also, wait for it to
1782 my $location_job = qsearchs('queue', {
1783 job => 'FS::cust_main::Location::process_upgrade_location'
1785 if ( $location_job ) {
1786 $job->depend_insert($location_job->jobnum);
1788 # Then mark the upgrade as done, so that we don't queue the job twice
1789 # and somehow run two of them concurrently.
1790 FS::upgrade_journal->set_done($upgrade);
1791 # This upgrade now does the job of assigning taxable_billpkgnums to
1792 # cust_bill_pkg_tax_location, so set that task done also.
1793 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1800 setup and recur shouldn't be separate fields. There should be one "amount"
1801 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1803 A line item with both should really be two separate records (preserving
1804 sdate and edate for setup fees for recurring packages - that information may
1805 be valuable later). Invoice generation (cust_main::bill), invoice printing
1806 (cust_bill), tax reports (report_tax.cgi) and line item reports
1807 (cust_bill_pkg.cgi) would need to be updated.
1809 owed_setup and owed_recur could then be repaced by just owed, and
1810 cust_bill::open_cust_bill_pkg and
1811 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1813 The upgrade procedure is pretty sketchy.
1817 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1818 from the base documentation.