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 if ( my $fee_origin = $self->get('fee_origin') ) {
304 $fee_origin->set('billpkgnum' => $self->billpkgnum);
305 $error = $fee_origin->replace;
307 $dbh->rollback if $oldAutoCommit;
308 return "error updating fee origin record: $error";
312 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
313 if ( $cust_tax_adjustment ) {
314 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
315 $error = $cust_tax_adjustment->replace;
317 $dbh->rollback if $oldAutoCommit;
318 return "error replacing cust_tax_adjustment: $error";
322 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
329 Voids this line item: deletes the line item and adds a record of the voided
330 line item to the FS::cust_bill_pkg_void table (and related tables).
336 my $reason = scalar(@_) ? shift : '';
338 local $SIG{HUP} = 'IGNORE';
339 local $SIG{INT} = 'IGNORE';
340 local $SIG{QUIT} = 'IGNORE';
341 local $SIG{TERM} = 'IGNORE';
342 local $SIG{TSTP} = 'IGNORE';
343 local $SIG{PIPE} = 'IGNORE';
345 my $oldAutoCommit = $FS::UID::AutoCommit;
346 local $FS::UID::AutoCommit = 0;
349 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
350 map { $_ => $self->get($_) } $self->fields
352 $cust_bill_pkg_void->reason($reason);
353 my $error = $cust_bill_pkg_void->insert;
355 $dbh->rollback if $oldAutoCommit;
359 foreach my $table (qw(
361 cust_bill_pkg_display
362 cust_bill_pkg_discount
363 cust_bill_pkg_tax_location
364 cust_bill_pkg_tax_rate_location
369 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
371 my $vclass = 'FS::'.$table.'_void';
372 my $void = $vclass->new( {
373 map { $_ => $linked->get($_) } $linked->fields
375 my $error = $void->insert || $linked->delete;
377 $dbh->rollback if $oldAutoCommit;
385 $error = $self->delete;
387 $dbh->rollback if $oldAutoCommit;
391 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
406 local $SIG{HUP} = 'IGNORE';
407 local $SIG{INT} = 'IGNORE';
408 local $SIG{QUIT} = 'IGNORE';
409 local $SIG{TERM} = 'IGNORE';
410 local $SIG{TSTP} = 'IGNORE';
411 local $SIG{PIPE} = 'IGNORE';
413 my $oldAutoCommit = $FS::UID::AutoCommit;
414 local $FS::UID::AutoCommit = 0;
417 foreach my $table (qw(
419 cust_bill_pkg_display
420 cust_bill_pkg_discount
421 cust_bill_pkg_tax_location
422 cust_bill_pkg_tax_rate_location
429 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
430 my $error = $linked->delete;
432 $dbh->rollback if $oldAutoCommit;
439 foreach my $cust_tax_adjustment (
440 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
442 $cust_tax_adjustment->billpkgnum(''); #NULL
443 my $error = $cust_tax_adjustment->replace;
445 $dbh->rollback if $oldAutoCommit;
450 my $error = $self->SUPER::delete(@_);
452 $dbh->rollback if $oldAutoCommit;
456 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
462 #alas, bin/follow-tax-rename
464 #=item replace OLD_RECORD
466 #Currently unimplemented. This would be even more of an accounting nightmare
467 #than deleteing the items. Just don't do it.
472 # return "Can't modify cust_bill_pkg records!";
477 Checks all fields to make sure this is a valid line item. If there is an
478 error, returns the error, otherwise returns false. Called by the insert
487 $self->ut_numbern('billpkgnum')
488 || $self->ut_snumber('pkgnum')
489 || $self->ut_number('invnum')
490 || $self->ut_money('setup')
491 || $self->ut_money('recur')
492 || $self->ut_numbern('sdate')
493 || $self->ut_numbern('edate')
494 || $self->ut_textn('itemdesc')
495 || $self->ut_textn('itemcomment')
496 || $self->ut_enum('hidden', [ '', 'Y' ])
498 return $error if $error;
500 $self->regularize_details;
502 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
503 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
504 return "Unknown pkgnum ". $self->pkgnum
505 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
508 return "Unknown invnum"
509 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
514 =item regularize_details
516 Converts the contents of the 'details' pseudo-field to
517 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
521 sub regularize_details {
523 if ( $self->get('details') ) {
524 foreach my $detail ( @{$self->get('details')} ) {
525 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
526 # then turn it into one
528 if ( ! ref($detail) ) {
529 $hash{'detail'} = $detail;
531 elsif ( ref($detail) eq 'HASH' ) {
534 elsif ( ref($detail) eq 'ARRAY' ) {
535 carp "passing invoice details as arrays is deprecated";
536 #carp "this way sucks, use a hash"; #but more useful/friendly
537 $hash{'format'} = $detail->[0];
538 $hash{'detail'} = $detail->[1];
539 $hash{'amount'} = $detail->[2];
540 $hash{'classnum'} = $detail->[3];
541 $hash{'phonenum'} = $detail->[4];
542 $hash{'accountcode'} = $detail->[5];
543 $hash{'startdate'} = $detail->[6];
544 $hash{'duration'} = $detail->[7];
545 $hash{'regionname'} = $detail->[8];
548 die "unknown detail type ". ref($detail);
550 $detail = new FS::cust_bill_pkg_detail \%hash;
552 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
558 =item set_exemptions TAXOBJECT, OPTIONS
560 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
561 L<FS::tax_rate> record for the tax.
563 This will deal with the following cases:
567 =item Fully exempt customers (cust_main.tax flag) or customer classes
570 =item Customers exempt from specific named taxes (cust_main_exemption
573 =item Taxes that don't apply to setup or recurring fees
574 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
576 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
579 =item Fees that aren't marked as taxable (part_fee.taxable).
583 It does NOT deal with monthly tax exemptions, which need more context
584 than this humble little method cares to deal with.
586 OPTIONS should include "custnum" => the customer number if this tax line
587 hasn't been inserted (which it probably hasn't).
589 Returns a list of exemption objects, which will also be attached to the
590 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
591 item will insert these records as well.
600 my $part_pkg = $self->part_pkg;
601 my $part_fee = $self->part_fee;
604 my $custnum = $opt{custnum};
605 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
607 $cust_main = FS::cust_main->by_key( $custnum )
608 or die "set_exemptions can't identify customer (pass custnum option)\n";
611 my $taxable_charged = $self->setup + $self->recur;
612 return unless $taxable_charged > 0;
614 ### Fully exempt customer ###
616 my $conf = FS::Conf->new;
617 if ( $conf->exists('cust_class-tax_exempt') ) {
618 my $cust_class = $cust_main->cust_class;
619 $exempt_cust = $cust_class->tax if $cust_class;
621 $exempt_cust = $cust_main->tax;
624 ### Exemption from named tax ###
625 my $exempt_cust_taxname;
626 if ( !$exempt_cust and $tax->taxname ) {
627 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
630 if ( $exempt_cust ) {
632 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
633 amount => $taxable_charged,
636 $taxable_charged = 0;
638 } elsif ( $exempt_cust_taxname ) {
640 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
641 amount => $taxable_charged,
642 exempt_cust_taxname => 'Y',
644 $taxable_charged = 0;
648 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
649 or ($part_pkg and $part_pkg->setuptax)
654 and $taxable_charged > 0 ) {
656 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
657 amount => $self->setup,
660 $taxable_charged -= $self->setup;
664 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
665 or ($part_pkg and $part_pkg->recurtax)
670 and $taxable_charged > 0 ) {
672 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
673 amount => $self->recur,
676 $taxable_charged -= $self->recur;
680 foreach (@new_exemptions) {
681 $_->set('taxnum', $tax->taxnum);
682 $_->set('taxtype', ref($tax));
685 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
686 return @new_exemptions;
692 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
698 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
703 Returns the customer (L<FS::cust_main> object) for this line item.
708 # required for cust_main_Mixin equivalence
709 # and use cust_bill instead of cust_pkg because this might not have a
712 my $cust_bill = $self->cust_bill or return '';
713 $cust_bill->cust_main;
716 =item previous_cust_bill_pkg
718 Returns the previous cust_bill_pkg for this package, if any.
722 sub previous_cust_bill_pkg {
724 return unless $self->sdate;
726 'table' => 'cust_bill_pkg',
727 'hashref' => { 'pkgnum' => $self->pkgnum,
728 'sdate' => { op=>'<', value=>$self->sdate },
730 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
736 Returns the amount owed (still outstanding) on this line item's setup fee,
737 which is the amount of the line item minus all payment applications (see
738 L<FS::cust_bill_pay_pkg> and credit applications (see
739 L<FS::cust_credit_bill_pkg>).
745 $self->owed('setup', @_);
750 Returns the amount owed (still outstanding) on this line item's recurring fee,
751 which is the amount of the line item minus all payment applications (see
752 L<FS::cust_bill_pay_pkg> and credit applications (see
753 L<FS::cust_credit_bill_pkg>).
759 $self->owed('recur', @_);
762 # modeled after cust_bill::owed...
764 my( $self, $field ) = @_;
765 my $balance = $self->$field();
766 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
767 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
768 $balance = sprintf( '%.2f', $balance );
769 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
775 my( $self, $field ) = @_;
776 my $balance = $self->$field();
777 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
778 $balance = sprintf( '%.2f', $balance );
779 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
783 sub cust_bill_pay_pkg {
784 my( $self, $field ) = @_;
785 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
786 'setuprecur' => $field,
791 sub cust_credit_bill_pkg {
792 my( $self, $field ) = @_;
793 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
794 'setuprecur' => $field,
801 Returns the number of billing units (for tax purposes) represented by this,
808 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
813 If this item has any discounts, returns a hashref in the format used
814 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
815 on an invoice. This will contain the keys 'description', 'amount',
816 'ext_description' (an arrayref of text lines describing the discounts),
817 and '_is_discount' (a flag).
819 The value for 'amount' will be negative, and will be scaled for the package
827 my $d; # this will be returned.
829 my @pkg_discounts = $self->pkg_discount;
830 if (@pkg_discounts) {
831 # special case: if there are old "discount details" on this line item,
832 # don't show discount line items
833 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
840 description => $self->mt('Discount'),
843 ext_description => \@ext,
844 pkgpart => $self->pkgpart,
845 feepart => $self->feepart,
846 # maybe should show quantity/unit discount?
848 foreach my $pkg_discount (@pkg_discounts) {
849 push @ext, $pkg_discount->description;
850 my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
851 $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
855 # show introductory rate as a pseudo-discount
856 if (!$d) { # this will conflict with showing real discounts
857 my $part_pkg = $self->part_pkg;
858 if ( $part_pkg and $part_pkg->option('show_as_discount') ) {
859 my $cust_pkg = $self->cust_pkg;
860 my $intro_end = $part_pkg->intro_end($cust_pkg);
861 my $_date = $self->cust_bill->_date;
862 if ( $intro_end > $_date ) {
863 $d = $part_pkg->item_discount($cust_pkg);
869 $d->{setup_amount} *= $self->quantity || 1; # ??
870 $d->{recur_amount} *= $self->quantity || 1; # ??
876 =item set_display OPTION => VALUE ...
878 A helper method for I<insert>, populates the pseudo-field B<display> with
879 appropriate FS::cust_bill_pkg_display objects.
881 Options are passed as a list of name/value pairs. Options are:
883 part_pkg: FS::part_pkg object from this line item's package.
885 real_pkgpart: if this line item comes from a bundled package, the pkgpart
886 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
891 my( $self, %opt ) = @_;
892 my $part_pkg = $opt{'part_pkg'};
893 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
895 my $conf = new FS::Conf;
897 # whether to break this down into setup/recur/usage
898 my $separate = $conf->exists('separate_usage');
900 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
901 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
903 # or use the category from $opt{'part_pkg'} if its not bundled?
904 my $categoryname = $cust_pkg->part_pkg->categoryname;
906 # if we don't have to separate setup/recur/usage, or put this in a
907 # package-specific section, or display a usage summary, then don't
908 # even create one of these. The item will just display in the unnamed
909 # section as a single line plus details.
910 return $self->set('display', [])
911 unless $separate || $categoryname || $usage_mandate;
915 my %hash = ( 'section' => $categoryname );
917 # whether to put usage details in a separate section, and if so, which one
918 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
919 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
921 # whether to show a usage summary line (total usage charges, no details)
922 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
923 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
926 # create lines for setup and (non-usage) recur, in the main section
927 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
928 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
930 # display everything in a single line
931 push @display, new FS::cust_bill_pkg_display
934 # and if usage_mandate is enabled, hide details
935 # (this only works on multisection invoices...)
936 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
940 if ($separate && $usage_section && $summary) {
941 # create a line for the usage summary in the main section
942 push @display, new FS::cust_bill_pkg_display { type => 'U',
948 if ($usage_mandate || ($usage_section && $summary) ) {
949 $hash{post_total} = 'Y';
952 if ($separate || $usage_mandate) {
953 # show call details for this line item in the usage section.
954 # if usage_mandate is on, this will display below the section subtotal.
955 # this also happens if usage is in a separate section and there's a
956 # summary in the main section, though I'm not sure why.
957 $hash{section} = $usage_section if $usage_section;
958 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
961 $self->set('display', \@display);
967 Returns a hash: keys are "setup", "recur" or usage classnum, values are
968 FS::cust_bill_pkg objects, each with no more than a single class (setup or
975 # XXX this goes away with cust_bill_pkg refactor
976 # or at least I wish it would, but it turns out to be harder than
979 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
980 my %cust_bill_pkg = ();
983 foreach my $classnum ($self->usage_classes) {
984 next if $classnum eq ''; # null-class usage is included in 'recur'
985 my $amount = $self->usage($classnum);
986 next if $amount == 0; # though if so we shouldn't be here
987 my $usage_item = FS::cust_bill_pkg->new({
991 'taxclass' => $classnum,
994 $cust_bill_pkg{$classnum} = $usage_item;
995 $usage_total += $amount;
998 foreach (qw(setup recur)) {
999 next if ($self->get($_) == 0);
1000 my $item = FS::cust_bill_pkg->new({
1007 $item->set($_, $self->get($_));
1008 $cust_bill_pkg{$_} = $item;
1012 $cust_bill_pkg{recur}->set('recur',
1013 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1020 =item usage CLASSNUM
1022 Returns the amount of the charge associated with usage class CLASSNUM if
1023 CLASSNUM is defined. Otherwise returns the total charge associated with
1029 my( $self, $classnum ) = @_;
1030 $self->regularize_details;
1032 if ( $self->get('details') ) {
1035 map { $_->amount || 0 }
1036 grep { !defined($classnum) or $classnum eq $_->classnum }
1037 @{ $self->get('details') }
1042 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1043 ' WHERE billpkgnum = '. $self->billpkgnum;
1044 if (defined $classnum) {
1045 if ($classnum =~ /^(\d+)$/) {
1046 $sql .= " AND classnum = $1";
1047 } elsif (defined($classnum) and $classnum eq '') {
1048 $sql .= " AND classnum IS NULL";
1052 my $sth = dbh->prepare($sql) or die dbh->errstr;
1053 $sth->execute or die $sth->errstr;
1055 return $sth->fetchrow_arrayref->[0] || 0;
1063 Returns a list of usage classnums associated with this invoice line's
1070 $self->regularize_details;
1072 if ( $self->get('details') ) {
1074 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1079 map { $_->classnum }
1080 qsearch({ table => 'cust_bill_pkg_detail',
1081 hashref => { billpkgnum => $self->billpkgnum },
1082 select => 'DISTINCT classnum',
1089 sub cust_tax_exempt_pkg {
1092 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1095 =item cust_bill_pkg_fee
1097 Returns the list of associated cust_bill_pkg_fee objects, if this is
1102 sub cust_bill_pkg_fee {
1104 qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
1107 =item cust_bill_pkg_tax_Xlocation
1109 Returns the list of associated cust_bill_pkg_tax_location and/or
1110 cust_bill_pkg_tax_rate_location objects
1114 sub cust_bill_pkg_tax_Xlocation {
1117 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1120 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1121 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1126 =item recur_show_zero
1128 Whether to show a zero recurring amount. This is true if the package or its
1129 definition has the recur_show_zero flag, and the recurring fee is actually
1130 zero for this period.
1134 sub recur_show_zero {
1135 my( $self, $what ) = @_;
1137 return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1139 $self->cust_pkg->_X_show_zero('recur');
1142 =item setup_show_zero
1144 Whether to show a zero setup charge. This requires the package or its
1145 definition to have the setup_show_zero flag, but it also returns false if
1146 the package's setup date is before this line item's start date.
1150 sub setup_show_zero {
1152 return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1153 my $cust_pkg = $self->cust_pkg;
1154 return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1155 return $cust_pkg->_X_show_zero('setup');
1158 =item credited [ BEFORE, AFTER, OPTIONS ]
1160 Returns the sum of credits applied to this item. Arguments are the same as
1161 owed_sql/paid_sql/credited_sql.
1167 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1170 =item tax_locationnum
1172 Returns the L<FS::cust_location> number that this line item is in for tax
1173 purposes. For package sales, it's the package tax location; for fees,
1174 it's the customer's default service location.
1178 sub tax_locationnum {
1180 if ( $self->pkgnum ) { # normal sales
1181 return $self->cust_pkg->tax_locationnum;
1182 } elsif ( $self->feepart and $self->invnum ) { # fees
1183 return $self->cust_bill->cust_main->ship_locationnum;
1191 if ( $self->pkgnum ) { # normal sales
1192 return $self->cust_pkg->tax_location;
1193 } elsif ( $self->feepart and $self->invnum ) { # fees
1194 return $self->cust_bill->cust_main->ship_location;
1202 =head1 CLASS METHODS
1208 Returns an SQL expression for the total usage charges in details on
1214 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1215 FROM cust_bill_pkg_detail
1216 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1218 sub usage_sql { $usage_sql }
1220 # this makes owed_sql, etc. much more concise
1222 my ($class, $start, $end, %opt) = @_;
1223 my $setuprecur = $opt{setuprecur} || '';
1225 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1226 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1227 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1229 if ($opt{no_usage} and $charged =~ /recur/) {
1230 $charged = "$charged - $usage_sql"
1237 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1239 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1240 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1241 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1247 '(' . $class->charged_sql(@_) .
1248 ' - ' . $class->paid_sql(@_) .
1249 ' - ' . $class->credited_sql(@_) . ')'
1252 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1254 Returns an SQL expression for the sum of payments applied to this item.
1259 my ($class, $start, $end, %opt) = @_;
1260 my $s = $start ? "AND cust_pay._date <= $start" : '';
1261 my $e = $end ? "AND cust_pay._date > $end" : '';
1262 my $setuprecur = $opt{setuprecur} || '';
1263 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1264 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1265 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1267 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1268 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1269 JOIN cust_pay USING (paynum)
1270 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1271 $s $e $setuprecur )";
1273 if ( $opt{no_usage} ) {
1274 # cap the amount paid at the sum of non-usage charges,
1275 # minus the amount credited against non-usage charges
1277 $class->charged_sql($start, $end, %opt) . ' - ' .
1278 $class->credited_sql($start, $end, %opt).')';
1287 my ($class, $start, $end, %opt) = @_;
1288 my $s = $start ? "AND cust_credit._date <= $start" : '';
1289 my $e = $end ? "AND cust_credit._date > $end" : '';
1290 my $setuprecur = $opt{setuprecur} || '';
1291 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1292 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1293 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1295 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1296 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1297 JOIN cust_credit USING (crednum)
1298 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1299 $s $e $setuprecur )";
1301 if ( $opt{no_usage} ) {
1302 # cap the amount credited at the sum of non-usage charges
1303 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1311 sub upgrade_tax_location {
1312 # For taxes that were calculated/invoiced before cust_location refactoring
1313 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1314 # they were calculated on a package-location basis. Create them here,
1315 # along with any necessary cust_location records and any tax exemption
1318 my ($class, %opt) = @_;
1319 # %opt may include 's' and 'e': start and end date ranges
1320 # and 'X': abort on any error, instead of just rolling back changes to
1323 my $oldAutoCommit = $FS::UID::AutoCommit;
1324 local $FS::UID::AutoCommit = 0;
1327 use FS::h_cust_main;
1328 use FS::h_cust_bill;
1330 use FS::h_cust_main_exemption;
1333 local $FS::cust_location::import = 1;
1335 my $conf = FS::Conf->new; # h_conf?
1336 return if $conf->exists('enable_taxproducts'); #don't touch this case
1337 my $use_ship = $conf->exists('tax-ship_address');
1338 my $use_pkgloc = $conf->exists('tax-pkg_address');
1340 my $date_where = '';
1342 $date_where .= " AND cust_bill._date >= $opt{s}";
1345 $date_where .= " AND cust_bill._date < $opt{e}";
1348 my $commit_each_invoice = 1 unless $opt{X};
1350 # if an invoice has either of these kinds of objects, then it doesn't
1351 # need to be upgraded...probably
1352 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1353 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1354 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1355 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1356 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1357 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1358 ' AND exempt_monthly IS NULL';
1360 my %all_tax_names = (
1363 map { $_->taxname => 1 }
1364 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1367 my $search = FS::Cursor->new({
1368 table => 'cust_bill',
1370 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1371 "AND NOT EXISTS($sub_has_exempt) ".
1375 #print "Processing ".scalar(@invnums)." invoices...\n";
1379 while (my $cust_bill = $search->fetch) {
1380 my $invnum = $cust_bill->invnum;
1382 print STDERR "Invoice #$invnum\n";
1384 my %pkgpart_taxclass; # pkgpart => taxclass
1385 my %pkgpart_exempt_setup;
1386 my %pkgpart_exempt_recur;
1387 my $h_cust_bill = qsearchs('h_cust_bill',
1388 { invnum => $invnum,
1389 history_action => 'insert' });
1390 if (!$h_cust_bill) {
1391 warn "no insert record for invoice $invnum; skipped\n";
1392 #$date = $cust_bill->_date as a fallback?
1393 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1394 # when looking up history records in other tables.
1397 my $custnum = $h_cust_bill->custnum;
1399 # Determine the address corresponding to this tax region.
1400 # It's either the bill or ship address of the customer as of the
1401 # invoice date-of-insertion. (Not necessarily the invoice date.)
1402 my $date = $h_cust_bill->history_date;
1403 my $h_cust_main = qsearchs('h_cust_main',
1404 { custnum => $custnum },
1405 FS::h_cust_main->sql_h_searchs($date)
1407 if (!$h_cust_main ) {
1408 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1410 # fallback to current $cust_main? sounds dangerous.
1413 # This is a historical customer record, so it has a historical address.
1414 # If there's no cust_location matching this custnum and address (there
1415 # probably isn't), create one.
1416 my %tax_loc; # keys are pkgnums, values are cust_location objects
1417 my $default_tax_loc;
1418 if ( $h_cust_main->bill_locationnum ) {
1419 # the location has already been upgraded
1421 $default_tax_loc = $h_cust_main->ship_location;
1423 $default_tax_loc = $h_cust_main->bill_location;
1426 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1427 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1428 FS::cust_main->location_fields;
1429 # not really needed for this, and often result in duplicate locations
1430 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1432 $hash{custnum} = $h_cust_main->custnum;
1433 $default_tax_loc = FS::cust_location->new(\%hash);
1434 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1436 warn "couldn't create historical location record for cust#".
1437 $h_cust_main->custnum.": $error\n";
1442 $exempt_cust = 1 if $h_cust_main->tax;
1444 # classify line items
1446 my %nontax_items; # taxclass => array of cust_bill_pkg
1447 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1448 my $pkgnum = $item->pkgnum;
1450 if ( $pkgnum == 0 ) {
1452 push @tax_items, $item;
1455 # (pkgparts really shouldn't change, right?)
1456 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1457 FS::h_cust_pkg->sql_h_searchs($date)
1459 if ( !$h_cust_pkg ) {
1460 warn "no historical package #".$item->pkgpart."; skipped\n";
1463 my $pkgpart = $h_cust_pkg->pkgpart;
1465 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1466 # then this package already had a locationnum assigned, and that's
1467 # the one to use for tax calculation
1468 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1470 # use the customer's bill or ship loc, which was inserted earlier
1471 $tax_loc{$pkgnum} = $default_tax_loc;
1474 if (!exists $pkgpart_taxclass{$pkgpart}) {
1475 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1476 FS::h_part_pkg->sql_h_searchs($date)
1478 if ( !$h_part_pkg ) {
1479 warn "no historical package def #$pkgpart; skipped\n";
1482 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1483 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1484 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1487 # mark any exemptions that apply
1488 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1489 $item->set('exempt_setup' => 1);
1492 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1493 $item->set('exempt_recur' => 1);
1496 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1498 $nontax_items{$taxclass} ||= [];
1499 push @{ $nontax_items{$taxclass} }, $item;
1503 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1506 # Get any per-customer taxname exemptions that were in effect.
1507 my %exempt_cust_taxname;
1508 foreach (keys %all_tax_names) {
1509 my $h_exemption = qsearchs('h_cust_main_exemption', {
1510 'custnum' => $custnum,
1513 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1516 $exempt_cust_taxname{ $_ } = 1;
1520 # Use a variation on the procedure in
1521 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1523 my @loc_keys = qw( district city county state country );
1524 my %taxdef_by_name; # by name, and then by taxclass
1525 my %est_tax; # by name, and then by taxclass
1526 my %taxable_items; # by taxnum, and then an array
1528 foreach my $taxclass (keys %nontax_items) {
1529 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1530 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1531 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1532 my @elim = qw( district city county state );
1533 my @taxdefs; # because there may be several with different taxnames
1535 $myhash{taxclass} = $taxclass;
1536 @taxdefs = qsearch('cust_main_county', \%myhash);
1538 $myhash{taxclass} = '';
1539 @taxdefs = qsearch('cust_main_county', \%myhash);
1541 $myhash{ shift @elim } = '';
1542 } while scalar(@elim) and !@taxdefs;
1544 foreach my $taxdef (@taxdefs) {
1545 next if $taxdef->tax == 0;
1546 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1548 $taxable_items{$taxdef->taxnum} ||= [];
1549 # clone the item so that taxdef-dependent changes don't
1550 # change it for other taxdefs
1551 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1553 # these flags are already set if the part_pkg declares itself exempt
1554 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1555 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1558 my $taxable = $item->setup + $item->recur;
1560 # h_cust_credit_bill_pkg?
1561 # NO. Because if these exemptions HAD been created at the time of
1562 # billing, and then a credit applied later, the exemption would
1563 # have been adjusted by the amount of the credit. So we adjust
1564 # the taxable amount before creating the exemption.
1565 # But don't deduct the credit from taxable, because the tax was
1566 # calculated before the credit was applied.
1567 foreach my $f (qw(setup recur)) {
1568 my $credited = FS::Record->scalar_sql(
1569 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1570 "WHERE billpkgnum = ? AND setuprecur = ?",
1574 $item->set($f, $item->get($f) - $credited) if $credited;
1576 my $existing_exempt = FS::Record->scalar_sql(
1577 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1578 "billpkgnum = ? AND taxnum = ?",
1579 $item->billpkgnum, $taxdef->taxnum
1581 $taxable -= $existing_exempt;
1583 if ( $taxable and $exempt_cust ) {
1584 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1587 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1588 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1591 if ( $taxable and $item->exempt_setup ) {
1592 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1593 $taxable -= $item->setup;
1595 if ( $taxable and $item->exempt_recur ) {
1596 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1597 $taxable -= $item->recur;
1600 $item->set('taxable' => $taxable);
1601 push @{ $taxable_items{$taxdef->taxnum} }, $item
1604 # estimate the amount of tax (this is necessary because different
1605 # taxdefs with the same taxname may have different tax rates)
1606 # and sum that for each taxname/taxclass combination
1608 $est_tax{$taxdef->taxname} ||= {};
1609 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1610 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1611 $taxable * $taxdef->tax;
1613 foreach (@new_exempt) {
1614 next if $_->{amount} == 0;
1615 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1617 billpkgnum => $item->billpkgnum,
1618 taxnum => $taxdef->taxnum,
1620 my $error = $cust_tax_exempt_pkg->insert;
1622 my $pkgnum = $item->pkgnum;
1623 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1627 } #foreach @new_exempt
1630 } #foreach $taxclass
1632 # Now go through the billed taxes and match them up with the line items.
1633 TAX_ITEM: foreach my $tax_item ( @tax_items )
1635 my $taxname = $tax_item->itemdesc;
1636 $taxname = '' if $taxname eq 'Tax';
1638 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1639 # then we didn't find any applicable taxes with this name
1640 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1641 # possibly all of these should be "next TAX_ITEM", but whole invoices
1642 # are transaction protected and we can go back and retry them.
1645 # classname => cust_main_county
1646 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1648 # Divide the tax item among taxclasses, if necessary
1649 # classname => estimated tax amount
1650 my $this_est_tax = $est_tax{$taxname};
1651 if (!defined $this_est_tax) {
1652 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1655 my $est_total = sum(values %$this_est_tax);
1656 if ( $est_total == 0 ) {
1658 warn "estimated tax on invoice #$invnum is zero.\n";
1662 my $real_tax = $tax_item->setup;
1663 printf ("Distributing \$%.2f tax:\n", $real_tax);
1664 my $cents_remaining = $real_tax * 100; # for rounding error
1665 my @tax_links; # partial CBPTL hashrefs
1666 foreach my $taxclass (keys %taxdef_by_class) {
1667 my $taxdef = $taxdef_by_class{$taxclass};
1668 # these items already have "taxable" set to their charge amount
1669 # after applying any credits or exemptions
1670 my @items = @{ $taxable_items{$taxdef->taxnum} };
1671 my $subtotal = sum(map {$_->get('taxable')} @items);
1672 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1674 foreach my $nontax (@items) {
1675 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1676 my $part = int($real_tax
1678 * ($this_est_tax->{$taxclass}/$est_total)
1680 * ($nontax->get('taxable'))/$subtotal
1684 $cents_remaining -= $part;
1686 taxnum => $taxdef->taxnum,
1687 pkgnum => $nontax->pkgnum,
1688 locationnum => $my_tax_loc->locationnum,
1689 billpkgnum => $nontax->billpkgnum,
1693 } #foreach $taxclass
1694 # Distribute any leftover tax round-robin style, one cent at a time.
1696 my $nlinks = scalar(@tax_links);
1698 # ensure that it really is an integer
1699 $cents_remaining = sprintf('%.0f', $cents_remaining);
1700 while ($cents_remaining > 0) {
1701 $tax_links[$i % $nlinks]->{cents} += 1;
1706 warn "Can't create tax links--no taxable items found.\n";
1710 # Gather credit/payment applications so that we can link them
1713 qsearch( 'cust_credit_bill_pkg',
1714 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1716 qsearch( 'cust_bill_pay_pkg',
1717 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1721 # grab the first one
1722 my $this_unlinked = shift @unlinked;
1723 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1725 # Create tax links (yay!)
1726 printf("Creating %d tax links.\n",scalar(@tax_links));
1727 foreach (@tax_links) {
1728 my $link = FS::cust_bill_pkg_tax_location->new({
1729 billpkgnum => $tax_item->billpkgnum,
1730 taxtype => 'FS::cust_main_county',
1731 locationnum => $_->{locationnum},
1732 taxnum => $_->{taxnum},
1733 pkgnum => $_->{pkgnum},
1734 amount => sprintf('%.2f', $_->{cents} / 100),
1735 taxable_billpkgnum => $_->{billpkgnum},
1737 my $error = $link->insert;
1739 warn "Can't create tax link for inv#$invnum: $error\n";
1743 my $link_cents = $_->{cents};
1744 # update/create subitem links
1746 # If $this_unlinked is undef, then we've allocated all of the
1747 # credit/payment applications to the tax item. If $link_cents is 0,
1748 # then we've applied credits/payments to all of this package fraction,
1749 # so go on to the next.
1750 while ($this_unlinked and $link_cents) {
1751 # apply as much as possible of $link_amount to this credit/payment
1753 my $apply_cents = min($link_cents, $unlinked_cents);
1754 $link_cents -= $apply_cents;
1755 $unlinked_cents -= $apply_cents;
1756 # $link_cents or $unlinked_cents or both are now zero
1757 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1758 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1759 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1760 if ( $this_unlinked->$pkey ) {
1761 # then it's an existing link--replace it
1762 $error = $this_unlinked->replace;
1764 $this_unlinked->insert;
1766 # what do we do with errors at this stage?
1768 warn "Error creating tax application link: $error\n";
1769 next INVOICE; # for lack of a better idea
1772 if ( $unlinked_cents == 0 ) {
1773 # then we've allocated all of this payment/credit application,
1774 # so grab the next one
1775 $this_unlinked = shift @unlinked;
1776 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1777 } elsif ( $link_cents == 0 ) {
1778 # then we've covered all of this package tax fraction, so split
1779 # off a new application from this one
1780 $this_unlinked = $this_unlinked->new({
1781 $this_unlinked->hash,
1784 # $unlinked_cents is still what it is
1787 } #while $this_unlinked and $link_cents
1788 } #foreach (@tax_links)
1789 } #foreach $tax_item
1791 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1797 $dbh->rollback if $oldAutoCommit;
1798 die "Upgrade halted.\n" unless $commit_each_invoice;
1802 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1807 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1811 use Date::Parse 'str2time';
1814 my $upgrade = 'tax_location_2012';
1815 return if FS::upgrade_journal->is_done($upgrade);
1816 my $job = FS::queue->new({
1817 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1819 # call it kind of like a class method, not that it matters much
1820 $job->insert($class, 's' => str2time('2012-01-01'));
1821 # if there's a customer location upgrade queued also, wait for it to
1823 my $location_job = qsearchs('queue', {
1824 job => 'FS::cust_main::Location::process_upgrade_location'
1826 if ( $location_job ) {
1827 $job->depend_insert($location_job->jobnum);
1829 # Then mark the upgrade as done, so that we don't queue the job twice
1830 # and somehow run two of them concurrently.
1831 FS::upgrade_journal->set_done($upgrade);
1832 # This upgrade now does the job of assigning taxable_billpkgnums to
1833 # cust_bill_pkg_tax_location, so set that task done also.
1834 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1841 setup and recur shouldn't be separate fields. There should be one "amount"
1842 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1844 A line item with both should really be two separate records (preserving
1845 sdate and edate for setup fees for recurring packages - that information may
1846 be valuable later). Invoice generation (cust_main::bill), invoice printing
1847 (cust_bill), tax reports (report_tax.cgi) and line item reports
1848 (cust_bill_pkg.cgi) would need to be updated.
1850 owed_setup and owed_recur could then be repaced by just owed, and
1851 cust_bill::open_cust_bill_pkg and
1852 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1854 The upgrade procedure is pretty sketchy.
1858 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1859 from the base documentation.