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 : '';
337 my $reprocess_cdrs = 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
369 my %delete_args = ();
370 $delete_args{'reprocess_cdrs'} = $reprocess_cdrs
371 if $table eq 'cust_bill_pkg_detail';
373 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
375 my $vclass = 'FS::'.$table.'_void';
376 my $void = $vclass->new( {
377 map { $_ => $linked->get($_) } $linked->fields
379 my $error = $void->insert || $linked->delete(%delete_args);
381 $dbh->rollback if $oldAutoCommit;
389 $error = $self->delete( skip_update_cust_bill_charged=>1 );
391 $dbh->rollback if $oldAutoCommit;
395 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
411 local $SIG{HUP} = 'IGNORE';
412 local $SIG{INT} = 'IGNORE';
413 local $SIG{QUIT} = 'IGNORE';
414 local $SIG{TERM} = 'IGNORE';
415 local $SIG{TSTP} = 'IGNORE';
416 local $SIG{PIPE} = 'IGNORE';
418 my $oldAutoCommit = $FS::UID::AutoCommit;
419 local $FS::UID::AutoCommit = 0;
422 foreach my $table (qw(
424 cust_bill_pkg_display
425 cust_bill_pkg_discount
426 cust_bill_pkg_tax_location
427 cust_bill_pkg_tax_rate_location
434 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
435 my $error = $linked->delete;
437 $dbh->rollback if $oldAutoCommit;
444 foreach my $cust_tax_adjustment (
445 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
447 $cust_tax_adjustment->billpkgnum(''); #NULL
448 my $error = $cust_tax_adjustment->replace;
450 $dbh->rollback if $oldAutoCommit;
455 unless ( $opt{skip_update_cust_bill_charged} ) {
457 #fix the invoice amount
459 my $cust_bill = $self->cust_bill;
460 my $charged = $cust_bill->charged - $self->setup - $self->recur;
461 $charged = sprintf('%.2f', $charged + 0.00000001 );
462 $cust_bill->charged( $charged );
464 #not adding a cc surcharge, but this override lets us modify charged
465 $cust_bill->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
467 my $error = $cust_bill->replace;
469 $dbh->rollback if $oldAutoCommit;
475 my $error = $self->SUPER::delete(@_);
477 $dbh->rollback if $oldAutoCommit;
481 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
487 #alas, bin/follow-tax-rename
489 #=item replace OLD_RECORD
491 #Currently unimplemented. This would be even more of an accounting nightmare
492 #than deleteing the items. Just don't do it.
497 # return "Can't modify cust_bill_pkg records!";
502 Checks all fields to make sure this is a valid line item. If there is an
503 error, returns the error, otherwise returns false. Called by the insert
512 $self->ut_numbern('billpkgnum')
513 || $self->ut_snumber('pkgnum')
514 || $self->ut_number('invnum')
515 || $self->ut_money('setup')
516 || $self->ut_money('recur')
517 || $self->ut_numbern('sdate')
518 || $self->ut_numbern('edate')
519 || $self->ut_textn('itemdesc')
520 || $self->ut_textn('itemcomment')
521 || $self->ut_enum('hidden', [ '', 'Y' ])
523 return $error if $error;
525 $self->regularize_details;
527 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
528 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
529 return "Unknown pkgnum ". $self->pkgnum
530 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
533 return "Unknown invnum"
534 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
539 =item regularize_details
541 Converts the contents of the 'details' pseudo-field to
542 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
546 sub regularize_details {
548 if ( $self->get('details') ) {
549 foreach my $detail ( @{$self->get('details')} ) {
550 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
551 # then turn it into one
553 if ( ! ref($detail) ) {
554 $hash{'detail'} = $detail;
556 elsif ( ref($detail) eq 'HASH' ) {
559 elsif ( ref($detail) eq 'ARRAY' ) {
560 carp "passing invoice details as arrays is deprecated";
561 #carp "this way sucks, use a hash"; #but more useful/friendly
562 $hash{'format'} = $detail->[0];
563 $hash{'detail'} = $detail->[1];
564 $hash{'amount'} = $detail->[2];
565 $hash{'classnum'} = $detail->[3];
566 $hash{'phonenum'} = $detail->[4];
567 $hash{'accountcode'} = $detail->[5];
568 $hash{'startdate'} = $detail->[6];
569 $hash{'duration'} = $detail->[7];
570 $hash{'regionname'} = $detail->[8];
573 die "unknown detail type ". ref($detail);
575 $detail = new FS::cust_bill_pkg_detail \%hash;
577 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
583 =item set_exemptions TAXOBJECT, OPTIONS
585 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
586 L<FS::tax_rate> record for the tax.
588 This will deal with the following cases:
592 =item Fully exempt customers (cust_main.tax flag) or customer classes
595 =item Customers exempt from specific named taxes (cust_main_exemption
598 =item Taxes that don't apply to setup or recurring fees
599 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
601 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
604 =item Fees that aren't marked as taxable (part_fee.taxable).
608 It does NOT deal with monthly tax exemptions, which need more context
609 than this humble little method cares to deal with.
611 OPTIONS should include "custnum" => the customer number if this tax line
612 hasn't been inserted (which it probably hasn't).
614 Returns a list of exemption objects, which will also be attached to the
615 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
616 item will insert these records as well.
625 my $part_pkg = $self->part_pkg;
626 my $part_fee = $self->part_fee;
629 my $custnum = $opt{custnum};
630 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
632 $cust_main = FS::cust_main->by_key( $custnum )
633 or die "set_exemptions can't identify customer (pass custnum option)\n";
636 my $taxable_charged = $self->setup + $self->recur;
637 return unless $taxable_charged > 0;
639 ### Fully exempt customer ###
641 my $conf = FS::Conf->new;
642 if ( $conf->exists('cust_class-tax_exempt') ) {
643 my $cust_class = $cust_main->cust_class;
644 $exempt_cust = $cust_class->tax if $cust_class;
646 $exempt_cust = $cust_main->tax;
649 ### Exemption from named tax ###
650 my $exempt_cust_taxname;
651 if ( !$exempt_cust and $tax->taxname ) {
652 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
655 if ( $exempt_cust ) {
657 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
658 amount => $taxable_charged,
661 $taxable_charged = 0;
663 } elsif ( $exempt_cust_taxname ) {
665 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
666 amount => $taxable_charged,
667 exempt_cust_taxname => 'Y',
669 $taxable_charged = 0;
673 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
674 or ($part_pkg and $part_pkg->setuptax)
679 and $taxable_charged > 0 ) {
681 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
682 amount => $self->setup,
685 $taxable_charged -= $self->setup;
689 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
690 or ($part_pkg and $part_pkg->recurtax)
695 and $taxable_charged > 0 ) {
697 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
698 amount => $self->recur,
701 $taxable_charged -= $self->recur;
705 foreach (@new_exemptions) {
706 $_->set('taxnum', $tax->taxnum);
707 $_->set('taxtype', ref($tax));
710 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
711 return @new_exemptions;
717 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
723 qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
728 Returns the customer (L<FS::cust_main> object) for this line item.
733 # required for cust_main_Mixin equivalence
734 # and use cust_bill instead of cust_pkg because this might not have a
737 my $cust_bill = $self->cust_bill or return '';
738 $cust_bill->cust_main;
741 =item previous_cust_bill_pkg
743 Returns the previous cust_bill_pkg for this package, if any.
747 sub previous_cust_bill_pkg {
749 return unless $self->sdate;
751 'table' => 'cust_bill_pkg',
752 'hashref' => { 'pkgnum' => $self->pkgnum,
753 'sdate' => { op=>'<', value=>$self->sdate },
755 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
761 Returns the amount owed (still outstanding) on this line item's setup fee,
762 which is the amount of the line item minus all payment applications (see
763 L<FS::cust_bill_pay_pkg> and credit applications (see
764 L<FS::cust_credit_bill_pkg>).
770 $self->owed('setup', @_);
775 Returns the amount owed (still outstanding) on this line item's recurring fee,
776 which is the amount of the line item minus all payment applications (see
777 L<FS::cust_bill_pay_pkg> and credit applications (see
778 L<FS::cust_credit_bill_pkg>).
784 $self->owed('recur', @_);
787 # modeled after cust_bill::owed...
789 my( $self, $field ) = @_;
790 my $balance = $self->$field();
791 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
792 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
793 $balance = sprintf( '%.2f', $balance );
794 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
800 my( $self, $field ) = @_;
801 my $balance = $self->$field();
802 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
803 $balance = sprintf( '%.2f', $balance );
804 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
808 sub cust_bill_pay_pkg {
809 my( $self, $field ) = @_;
810 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
811 'setuprecur' => $field,
816 sub cust_credit_bill_pkg {
817 my( $self, $field ) = @_;
818 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
819 'setuprecur' => $field,
826 Returns the number of billing units (for tax purposes) represented by this,
833 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
838 If this item has any discounts, returns a hashref in the format used
839 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
840 on an invoice. This will contain the keys 'description', 'amount',
841 'ext_description' (an arrayref of text lines describing the discounts),
842 and '_is_discount' (a flag).
844 The value for 'amount' will be negative, and will be scaled for the package
852 my $d; # this will be returned.
854 my @pkg_discounts = $self->pkg_discount;
855 if (@pkg_discounts) {
856 # special case: if there are old "discount details" on this line item,
857 # don't show discount line items
858 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
865 description => $self->mt('Discount'),
867 ext_description => \@ext,
868 pkgpart => $self->pkgpart,
869 feepart => $self->feepart,
870 # maybe should show quantity/unit discount?
872 foreach my $pkg_discount (@pkg_discounts) {
873 push @ext, $pkg_discount->description;
874 $d->{'amount'} -= $pkg_discount->amount;
878 # show introductory rate as a pseudo-discount
879 if (!$d) { # this will conflict with showing real discounts
880 my $part_pkg = $self->part_pkg;
881 if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
882 my $cust_pkg = $self->cust_pkg;
883 my $intro_end = $part_pkg->intro_end($cust_pkg);
884 my $_date = $self->cust_bill->_date;
885 if ( $intro_end > $_date ) {
886 $d = $part_pkg->item_discount($cust_pkg);
892 $d->{amount} *= $self->quantity || 1;
898 =item set_display OPTION => VALUE ...
900 A helper method for I<insert>, populates the pseudo-field B<display> with
901 appropriate FS::cust_bill_pkg_display objects.
903 Options are passed as a list of name/value pairs. Options are:
905 part_pkg: FS::part_pkg object from this line item's package.
907 real_pkgpart: if this line item comes from a bundled package, the pkgpart
908 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
913 my( $self, %opt ) = @_;
914 my $part_pkg = $opt{'part_pkg'};
915 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
917 my $conf = new FS::Conf;
919 # whether to break this down into setup/recur/usage
920 my $separate = $conf->exists('separate_usage');
922 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
923 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
925 # or use the category from $opt{'part_pkg'} if its not bundled?
926 my $categoryname = $cust_pkg->part_pkg->categoryname;
928 # if we don't have to separate setup/recur/usage, or put this in a
929 # package-specific section, or display a usage summary, then don't
930 # even create one of these. The item will just display in the unnamed
931 # section as a single line plus details.
932 return $self->set('display', [])
933 unless $separate || $categoryname || $usage_mandate;
937 my %hash = ( 'section' => $categoryname );
939 # whether to put usage details in a separate section, and if so, which one
940 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
941 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
943 # whether to show a usage summary line (total usage charges, no details)
944 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
945 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
948 # create lines for setup and (non-usage) recur, in the main section
949 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
950 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
952 # display everything in a single line
953 push @display, new FS::cust_bill_pkg_display
956 # and if usage_mandate is enabled, hide details
957 # (this only works on multisection invoices...)
958 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
962 if ($separate && $usage_section && $summary) {
963 # create a line for the usage summary in the main section
964 push @display, new FS::cust_bill_pkg_display { type => 'U',
970 if ($usage_mandate || ($usage_section && $summary) ) {
971 $hash{post_total} = 'Y';
974 if ($separate || $usage_mandate) {
975 # show call details for this line item in the usage section.
976 # if usage_mandate is on, this will display below the section subtotal.
977 # this also happens if usage is in a separate section and there's a
978 # summary in the main section, though I'm not sure why.
979 $hash{section} = $usage_section if $usage_section;
980 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
983 $self->set('display', \@display);
989 Returns a hash: keys are "setup", "recur" or usage classnum, values are
990 FS::cust_bill_pkg objects, each with no more than a single class (setup or
997 # XXX this goes away with cust_bill_pkg refactor
998 # or at least I wish it would, but it turns out to be harder than
1001 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
1002 my %cust_bill_pkg = ();
1005 foreach my $classnum ($self->usage_classes) {
1006 next if $classnum eq ''; # null-class usage is included in 'recur'
1007 my $amount = $self->usage($classnum);
1008 next if $amount == 0; # though if so we shouldn't be here
1009 my $usage_item = FS::cust_bill_pkg->new({
1013 'taxclass' => $classnum,
1016 $cust_bill_pkg{$classnum} = $usage_item;
1017 $usage_total += $amount;
1020 foreach (qw(setup recur)) {
1021 next if ($self->get($_) == 0);
1022 my $item = FS::cust_bill_pkg->new({
1029 $item->set($_, $self->get($_));
1030 $cust_bill_pkg{$_} = $item;
1034 $cust_bill_pkg{recur}->set('recur',
1035 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1042 =item usage CLASSNUM
1044 Returns the amount of the charge associated with usage class CLASSNUM if
1045 CLASSNUM is defined. Otherwise returns the total charge associated with
1051 my( $self, $classnum ) = @_;
1052 $self->regularize_details;
1054 if ( $self->get('details') ) {
1057 map { $_->amount || 0 }
1058 grep { !defined($classnum) or $classnum eq $_->classnum }
1059 @{ $self->get('details') }
1064 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1065 ' WHERE billpkgnum = '. $self->billpkgnum;
1066 if (defined $classnum) {
1067 if ($classnum =~ /^(\d+)$/) {
1068 $sql .= " AND classnum = $1";
1069 } elsif (defined($classnum) and $classnum eq '') {
1070 $sql .= " AND classnum IS NULL";
1074 my $sth = dbh->prepare($sql) or die dbh->errstr;
1075 $sth->execute or die $sth->errstr;
1077 return $sth->fetchrow_arrayref->[0] || 0;
1085 Returns a list of usage classnums associated with this invoice line's
1092 $self->regularize_details;
1094 if ( $self->get('details') ) {
1096 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1101 map { $_->classnum }
1102 qsearch({ table => 'cust_bill_pkg_detail',
1103 hashref => { billpkgnum => $self->billpkgnum },
1104 select => 'DISTINCT classnum',
1111 sub cust_tax_exempt_pkg {
1114 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1117 =item cust_bill_pkg_fee
1119 Returns the list of associated cust_bill_pkg_fee objects, if this is
1124 sub cust_bill_pkg_fee {
1126 qsearch('cust_bill_pkg_fee', { billpkgnum => $self->billpkgnum });
1129 =item cust_bill_pkg_tax_Xlocation
1131 Returns the list of associated cust_bill_pkg_tax_location and/or
1132 cust_bill_pkg_tax_rate_location objects
1136 sub cust_bill_pkg_tax_Xlocation {
1139 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1142 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1143 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1148 =item recur_show_zero
1150 Whether to show a zero recurring amount. This is true if the package or its
1151 definition has the recur_show_zero flag, and the recurring fee is actually
1152 zero for this period.
1156 sub recur_show_zero {
1157 my( $self, $what ) = @_;
1159 return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1161 $self->cust_pkg->_X_show_zero('recur');
1164 =item setup_show_zero
1166 Whether to show a zero setup charge. This requires the package or its
1167 definition to have the setup_show_zero flag, but it also returns false if
1168 the package's setup date is before this line item's start date.
1172 sub setup_show_zero {
1174 return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1175 my $cust_pkg = $self->cust_pkg;
1176 return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1177 return $cust_pkg->_X_show_zero('setup');
1180 =item credited [ BEFORE, AFTER, OPTIONS ]
1182 Returns the sum of credits applied to this item. Arguments are the same as
1183 owed_sql/paid_sql/credited_sql.
1189 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1192 =item tax_locationnum
1194 Returns the L<FS::cust_location> number that this line item is in for tax
1195 purposes. For package sales, it's the package tax location; for fees,
1196 it's the customer's default service location.
1200 sub tax_locationnum {
1202 if ( $self->pkgnum ) { # normal sales
1203 return $self->cust_pkg->tax_locationnum;
1204 } elsif ( $self->feepart and $self->invnum ) { # fees
1205 return $self->cust_bill->cust_main->ship_locationnum;
1213 if ( $self->pkgnum ) { # normal sales
1214 return $self->cust_pkg->tax_location;
1215 } elsif ( $self->feepart and $self->invnum ) { # fees
1216 return $self->cust_bill->cust_main->ship_location;
1224 =head1 CLASS METHODS
1230 Returns an SQL expression for the total usage charges in details on
1236 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1237 FROM cust_bill_pkg_detail
1238 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1240 sub usage_sql { $usage_sql }
1242 # this makes owed_sql, etc. much more concise
1244 my ($class, $start, $end, %opt) = @_;
1245 my $setuprecur = $opt{setuprecur} || '';
1247 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1248 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1249 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1251 if ($opt{no_usage} and $charged =~ /recur/) {
1252 $charged = "$charged - $usage_sql"
1259 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1261 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1262 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1263 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1269 '(' . $class->charged_sql(@_) .
1270 ' - ' . $class->paid_sql(@_) .
1271 ' - ' . $class->credited_sql(@_) . ')'
1274 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1276 Returns an SQL expression for the sum of payments applied to this item.
1281 my ($class, $start, $end, %opt) = @_;
1282 my $s = $start ? "AND cust_pay._date <= $start" : '';
1283 my $e = $end ? "AND cust_pay._date > $end" : '';
1284 my $setuprecur = $opt{setuprecur} || '';
1285 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1286 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1287 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1289 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1290 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1291 JOIN cust_pay USING (paynum)
1292 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1293 $s $e $setuprecur )";
1295 if ( $opt{no_usage} ) {
1296 # cap the amount paid at the sum of non-usage charges,
1297 # minus the amount credited against non-usage charges
1299 $class->charged_sql($start, $end, %opt) . ' - ' .
1300 $class->credited_sql($start, $end, %opt).')';
1309 my ($class, $start, $end, %opt) = @_;
1310 my $s = $start ? "AND cust_credit._date <= $start" : '';
1311 my $e = $end ? "AND cust_credit._date > $end" : '';
1312 my $setuprecur = $opt{setuprecur} || '';
1313 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1314 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1315 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1317 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1318 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1319 JOIN cust_credit USING (crednum)
1320 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1321 $s $e $setuprecur )";
1323 if ( $opt{no_usage} ) {
1324 # cap the amount credited at the sum of non-usage charges
1325 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1333 sub upgrade_tax_location {
1334 # For taxes that were calculated/invoiced before cust_location refactoring
1335 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1336 # they were calculated on a package-location basis. Create them here,
1337 # along with any necessary cust_location records and any tax exemption
1340 my ($class, %opt) = @_;
1341 # %opt may include 's' and 'e': start and end date ranges
1342 # and 'X': abort on any error, instead of just rolling back changes to
1345 my $oldAutoCommit = $FS::UID::AutoCommit;
1346 local $FS::UID::AutoCommit = 0;
1349 use FS::h_cust_main;
1350 use FS::h_cust_bill;
1352 use FS::h_cust_main_exemption;
1355 local $FS::cust_location::import = 1;
1357 my $conf = FS::Conf->new; # h_conf?
1358 return if $conf->exists('enable_taxproducts'); #don't touch this case
1359 my $use_ship = $conf->exists('tax-ship_address');
1360 my $use_pkgloc = $conf->exists('tax-pkg_address');
1362 my $date_where = '';
1364 $date_where .= " AND cust_bill._date >= $opt{s}";
1367 $date_where .= " AND cust_bill._date < $opt{e}";
1370 my $commit_each_invoice = 1 unless $opt{X};
1372 # if an invoice has either of these kinds of objects, then it doesn't
1373 # need to be upgraded...probably
1374 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1375 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1376 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1377 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1378 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1379 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1380 ' AND exempt_monthly IS NULL';
1382 my %all_tax_names = (
1385 map { $_->taxname => 1 }
1386 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1389 my $search = FS::Cursor->new({
1390 table => 'cust_bill',
1392 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1393 "AND NOT EXISTS($sub_has_exempt) ".
1397 #print "Processing ".scalar(@invnums)." invoices...\n";
1401 while (my $cust_bill = $search->fetch) {
1402 my $invnum = $cust_bill->invnum;
1404 print STDERR "Invoice #$invnum\n";
1406 my %pkgpart_taxclass; # pkgpart => taxclass
1407 my %pkgpart_exempt_setup;
1408 my %pkgpart_exempt_recur;
1409 my $h_cust_bill = qsearchs('h_cust_bill',
1410 { invnum => $invnum,
1411 history_action => 'insert' });
1412 if (!$h_cust_bill) {
1413 warn "no insert record for invoice $invnum; skipped\n";
1414 #$date = $cust_bill->_date as a fallback?
1415 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1416 # when looking up history records in other tables.
1419 my $custnum = $h_cust_bill->custnum;
1421 # Determine the address corresponding to this tax region.
1422 # It's either the bill or ship address of the customer as of the
1423 # invoice date-of-insertion. (Not necessarily the invoice date.)
1424 my $date = $h_cust_bill->history_date;
1425 my $h_cust_main = qsearchs('h_cust_main',
1426 { custnum => $custnum },
1427 FS::h_cust_main->sql_h_searchs($date)
1429 if (!$h_cust_main ) {
1430 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1432 # fallback to current $cust_main? sounds dangerous.
1435 # This is a historical customer record, so it has a historical address.
1436 # If there's no cust_location matching this custnum and address (there
1437 # probably isn't), create one.
1438 my %tax_loc; # keys are pkgnums, values are cust_location objects
1439 my $default_tax_loc;
1440 if ( $h_cust_main->bill_locationnum ) {
1441 # the location has already been upgraded
1443 $default_tax_loc = $h_cust_main->ship_location;
1445 $default_tax_loc = $h_cust_main->bill_location;
1448 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1449 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1450 FS::cust_main->location_fields;
1451 # not really needed for this, and often result in duplicate locations
1452 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1454 $hash{custnum} = $h_cust_main->custnum;
1455 $default_tax_loc = FS::cust_location->new(\%hash);
1456 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1458 warn "couldn't create historical location record for cust#".
1459 $h_cust_main->custnum.": $error\n";
1464 $exempt_cust = 1 if $h_cust_main->tax;
1466 # classify line items
1468 my %nontax_items; # taxclass => array of cust_bill_pkg
1469 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1470 my $pkgnum = $item->pkgnum;
1472 if ( $pkgnum == 0 ) {
1474 push @tax_items, $item;
1477 # (pkgparts really shouldn't change, right?)
1478 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1479 FS::h_cust_pkg->sql_h_searchs($date)
1481 if ( !$h_cust_pkg ) {
1482 warn "no historical package #".$item->pkgpart."; skipped\n";
1485 my $pkgpart = $h_cust_pkg->pkgpart;
1487 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1488 # then this package already had a locationnum assigned, and that's
1489 # the one to use for tax calculation
1490 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1492 # use the customer's bill or ship loc, which was inserted earlier
1493 $tax_loc{$pkgnum} = $default_tax_loc;
1496 if (!exists $pkgpart_taxclass{$pkgpart}) {
1497 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1498 FS::h_part_pkg->sql_h_searchs($date)
1500 if ( !$h_part_pkg ) {
1501 warn "no historical package def #$pkgpart; skipped\n";
1504 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1505 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1506 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1509 # mark any exemptions that apply
1510 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1511 $item->set('exempt_setup' => 1);
1514 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1515 $item->set('exempt_recur' => 1);
1518 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1520 $nontax_items{$taxclass} ||= [];
1521 push @{ $nontax_items{$taxclass} }, $item;
1525 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1528 # Get any per-customer taxname exemptions that were in effect.
1529 my %exempt_cust_taxname;
1530 foreach (keys %all_tax_names) {
1531 my $h_exemption = qsearchs('h_cust_main_exemption', {
1532 'custnum' => $custnum,
1535 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1538 $exempt_cust_taxname{ $_ } = 1;
1542 # Use a variation on the procedure in
1543 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1545 my @loc_keys = qw( district city county state country );
1546 my %taxdef_by_name; # by name, and then by taxclass
1547 my %est_tax; # by name, and then by taxclass
1548 my %taxable_items; # by taxnum, and then an array
1550 foreach my $taxclass (keys %nontax_items) {
1551 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1552 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1553 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1554 my @elim = qw( district city county state );
1555 my @taxdefs; # because there may be several with different taxnames
1557 $myhash{taxclass} = $taxclass;
1558 @taxdefs = qsearch('cust_main_county', \%myhash);
1560 $myhash{taxclass} = '';
1561 @taxdefs = qsearch('cust_main_county', \%myhash);
1563 $myhash{ shift @elim } = '';
1564 } while scalar(@elim) and !@taxdefs;
1566 foreach my $taxdef (@taxdefs) {
1567 next if $taxdef->tax == 0;
1568 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1570 $taxable_items{$taxdef->taxnum} ||= [];
1571 # clone the item so that taxdef-dependent changes don't
1572 # change it for other taxdefs
1573 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1575 # these flags are already set if the part_pkg declares itself exempt
1576 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1577 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1580 my $taxable = $item->setup + $item->recur;
1582 # h_cust_credit_bill_pkg?
1583 # NO. Because if these exemptions HAD been created at the time of
1584 # billing, and then a credit applied later, the exemption would
1585 # have been adjusted by the amount of the credit. So we adjust
1586 # the taxable amount before creating the exemption.
1587 # But don't deduct the credit from taxable, because the tax was
1588 # calculated before the credit was applied.
1589 foreach my $f (qw(setup recur)) {
1590 my $credited = FS::Record->scalar_sql(
1591 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1592 "WHERE billpkgnum = ? AND setuprecur = ?",
1596 $item->set($f, $item->get($f) - $credited) if $credited;
1598 my $existing_exempt = FS::Record->scalar_sql(
1599 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1600 "billpkgnum = ? AND taxnum = ?",
1601 $item->billpkgnum, $taxdef->taxnum
1603 $taxable -= $existing_exempt;
1605 if ( $taxable and $exempt_cust ) {
1606 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1609 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1610 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1613 if ( $taxable and $item->exempt_setup ) {
1614 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1615 $taxable -= $item->setup;
1617 if ( $taxable and $item->exempt_recur ) {
1618 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1619 $taxable -= $item->recur;
1622 $item->set('taxable' => $taxable);
1623 push @{ $taxable_items{$taxdef->taxnum} }, $item
1626 # estimate the amount of tax (this is necessary because different
1627 # taxdefs with the same taxname may have different tax rates)
1628 # and sum that for each taxname/taxclass combination
1630 $est_tax{$taxdef->taxname} ||= {};
1631 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1632 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1633 $taxable * $taxdef->tax;
1635 foreach (@new_exempt) {
1636 next if $_->{amount} == 0;
1637 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1639 billpkgnum => $item->billpkgnum,
1640 taxnum => $taxdef->taxnum,
1642 my $error = $cust_tax_exempt_pkg->insert;
1644 my $pkgnum = $item->pkgnum;
1645 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1649 } #foreach @new_exempt
1652 } #foreach $taxclass
1654 # Now go through the billed taxes and match them up with the line items.
1655 TAX_ITEM: foreach my $tax_item ( @tax_items )
1657 my $taxname = $tax_item->itemdesc;
1658 $taxname = '' if $taxname eq 'Tax';
1660 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1661 # then we didn't find any applicable taxes with this name
1662 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1663 # possibly all of these should be "next TAX_ITEM", but whole invoices
1664 # are transaction protected and we can go back and retry them.
1667 # classname => cust_main_county
1668 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1670 # Divide the tax item among taxclasses, if necessary
1671 # classname => estimated tax amount
1672 my $this_est_tax = $est_tax{$taxname};
1673 if (!defined $this_est_tax) {
1674 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1677 my $est_total = sum(values %$this_est_tax);
1678 if ( $est_total == 0 ) {
1680 warn "estimated tax on invoice #$invnum is zero.\n";
1684 my $real_tax = $tax_item->setup;
1685 printf ("Distributing \$%.2f tax:\n", $real_tax);
1686 my $cents_remaining = $real_tax * 100; # for rounding error
1687 my @tax_links; # partial CBPTL hashrefs
1688 foreach my $taxclass (keys %taxdef_by_class) {
1689 my $taxdef = $taxdef_by_class{$taxclass};
1690 # these items already have "taxable" set to their charge amount
1691 # after applying any credits or exemptions
1692 my @items = @{ $taxable_items{$taxdef->taxnum} };
1693 my $subtotal = sum(map {$_->get('taxable')} @items);
1694 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1696 foreach my $nontax (@items) {
1697 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1698 my $part = int($real_tax
1700 * ($this_est_tax->{$taxclass}/$est_total)
1702 * ($nontax->get('taxable'))/$subtotal
1706 $cents_remaining -= $part;
1708 taxnum => $taxdef->taxnum,
1709 pkgnum => $nontax->pkgnum,
1710 locationnum => $my_tax_loc->locationnum,
1711 billpkgnum => $nontax->billpkgnum,
1715 } #foreach $taxclass
1716 # Distribute any leftover tax round-robin style, one cent at a time.
1718 my $nlinks = scalar(@tax_links);
1720 # ensure that it really is an integer
1721 $cents_remaining = sprintf('%.0f', $cents_remaining);
1722 while ($cents_remaining > 0) {
1723 $tax_links[$i % $nlinks]->{cents} += 1;
1728 warn "Can't create tax links--no taxable items found.\n";
1732 # Gather credit/payment applications so that we can link them
1735 qsearch( 'cust_credit_bill_pkg',
1736 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1738 qsearch( 'cust_bill_pay_pkg',
1739 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1743 # grab the first one
1744 my $this_unlinked = shift @unlinked;
1745 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1747 # Create tax links (yay!)
1748 printf("Creating %d tax links.\n",scalar(@tax_links));
1749 foreach (@tax_links) {
1750 my $link = FS::cust_bill_pkg_tax_location->new({
1751 billpkgnum => $tax_item->billpkgnum,
1752 taxtype => 'FS::cust_main_county',
1753 locationnum => $_->{locationnum},
1754 taxnum => $_->{taxnum},
1755 pkgnum => $_->{pkgnum},
1756 amount => sprintf('%.2f', $_->{cents} / 100),
1757 taxable_billpkgnum => $_->{billpkgnum},
1759 my $error = $link->insert;
1761 warn "Can't create tax link for inv#$invnum: $error\n";
1765 my $link_cents = $_->{cents};
1766 # update/create subitem links
1768 # If $this_unlinked is undef, then we've allocated all of the
1769 # credit/payment applications to the tax item. If $link_cents is 0,
1770 # then we've applied credits/payments to all of this package fraction,
1771 # so go on to the next.
1772 while ($this_unlinked and $link_cents) {
1773 # apply as much as possible of $link_amount to this credit/payment
1775 my $apply_cents = min($link_cents, $unlinked_cents);
1776 $link_cents -= $apply_cents;
1777 $unlinked_cents -= $apply_cents;
1778 # $link_cents or $unlinked_cents or both are now zero
1779 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1780 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1781 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1782 if ( $this_unlinked->$pkey ) {
1783 # then it's an existing link--replace it
1784 $error = $this_unlinked->replace;
1786 $this_unlinked->insert;
1788 # what do we do with errors at this stage?
1790 warn "Error creating tax application link: $error\n";
1791 next INVOICE; # for lack of a better idea
1794 if ( $unlinked_cents == 0 ) {
1795 # then we've allocated all of this payment/credit application,
1796 # so grab the next one
1797 $this_unlinked = shift @unlinked;
1798 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1799 } elsif ( $link_cents == 0 ) {
1800 # then we've covered all of this package tax fraction, so split
1801 # off a new application from this one
1802 $this_unlinked = $this_unlinked->new({
1803 $this_unlinked->hash,
1806 # $unlinked_cents is still what it is
1809 } #while $this_unlinked and $link_cents
1810 } #foreach (@tax_links)
1811 } #foreach $tax_item
1813 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1819 $dbh->rollback if $oldAutoCommit;
1820 die "Upgrade halted.\n" unless $commit_each_invoice;
1824 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1829 # Return an array of hashrefs for each cust_bill_pkg_tax_location
1830 # applied to this bill for this cust_bill_pkg.pkgnum.
1833 # In some situations, this list will contain more tax records than the
1834 # ones directly related to $self->billpkgnum. The returned list contains
1835 # all records, for this bill, charged against this billpkgnum's pkgnum.
1837 # One must keep this in mind when using data returned by this method.
1839 # An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
1840 # this necessary: When a linked-hidden package generates a tax/fee as a row
1841 # in cust_bill_pkg_tax_location, there is not enough information to surmise
1842 # with specificity which billpkgnum row represents the direct parent of the
1843 # the linked-hidden package's tax row. The closest we can get to this
1844 # backwards reassociation is to use the pkgnum. Therefore, when multiple
1845 # billpkgnum's appear with the same pkgnum, this method is going to return
1846 # the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
1848 # This could be addressed with an update to the model, and to the billing
1849 # routine that generates rows into cust_bill_pkg_tax_location. Perhaps a
1850 # column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
1851 # doing that now, because there would be no possible repair of data stored
1852 # historically prior to such a fix. I need _pkg_tax_list() to not be
1853 # broken for already-generated bills.
1855 # Any code you write relying on _pkg_tax_list() MUST be aware of, and
1856 # account for, the possible return of duplicated tax records returned
1857 # when method is called on multiple cust_bill_pkg_tax_location rows.
1858 # Duplicates can be identified by billpkgtaxlocationnum column.
1861 return unless $self->pkgnum;
1864 billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
1865 billpkgnum => $_->billpkgnum,
1866 taxnum => $_->taxnum,
1867 amount => $_->amount,
1868 taxname => $_->taxname,
1871 table => 'cust_bill_pkg_tax_location',
1873 LEFT JOIN cust_bill_pkg
1874 ON cust_bill_pkg.billpkgnum
1875 = cust_bill_pkg_tax_location.taxable_billpkgnum
1877 select => join( ', ', (qw|
1878 cust_bill_pkg.billpkgnum
1879 cust_bill_pkg_tax_location.billpkgtaxlocationnum
1880 cust_bill_pkg_tax_location.taxnum
1881 cust_bill_pkg_tax_location.amount
1885 ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
1887 ' cust_bill_pkg_tax_location.pkgnum = ' . dbh->quote( $self->pkgnum ),
1893 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1897 use Date::Parse 'str2time';
1900 my $upgrade = 'tax_location_2012';
1901 return if FS::upgrade_journal->is_done($upgrade);
1902 my $job = FS::queue->new({
1903 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1905 # call it kind of like a class method, not that it matters much
1906 $job->insert($class, 's' => str2time('2012-01-01'));
1907 # if there's a customer location upgrade queued also, wait for it to
1909 my $location_job = qsearchs('queue', {
1910 job => 'FS::cust_main::Location::process_upgrade_location'
1912 if ( $location_job ) {
1913 $job->depend_insert($location_job->jobnum);
1915 # Then mark the upgrade as done, so that we don't queue the job twice
1916 # and somehow run two of them concurrently.
1917 FS::upgrade_journal->set_done($upgrade);
1918 # This upgrade now does the job of assigning taxable_billpkgnums to
1919 # cust_bill_pkg_tax_location, so set that task done also.
1920 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1927 setup and recur shouldn't be separate fields. There should be one "amount"
1928 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1930 A line item with both should really be two separate records (preserving
1931 sdate and edate for setup fees for recurring packages - that information may
1932 be valuable later). Invoice generation (cust_main::bill), invoice printing
1933 (cust_bill), tax reports (report_tax.cgi) and line item reports
1934 (cust_bill_pkg.cgi) would need to be updated.
1936 owed_setup and owed_recur could then be repaced by just owed, and
1937 cust_bill::open_cust_bill_pkg and
1938 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1940 The upgrade procedure is pretty sketchy.
1944 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1945 from the base documentation.