1 package FS::cust_bill_pkg;
2 use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
5 use vars qw( @ISA $DEBUG $me );
7 use List::Util qw( sum min );
9 use FS::Record qw( qsearch qsearchs dbh );
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pkg_discount;
14 use FS::cust_bill_pkg_fee;
15 use FS::cust_bill_pay_pkg;
16 use FS::cust_credit_bill_pkg;
17 use FS::cust_tax_exempt_pkg;
18 use FS::cust_bill_pkg_tax_location;
19 use FS::cust_bill_pkg_tax_rate_location;
20 use FS::cust_tax_adjustment;
21 use FS::cust_bill_pkg_void;
22 use FS::cust_bill_pkg_detail_void;
23 use FS::cust_bill_pkg_display_void;
24 use FS::cust_bill_pkg_discount_void;
25 use FS::cust_bill_pkg_tax_location_void;
26 use FS::cust_bill_pkg_tax_rate_location_void;
27 use FS::cust_tax_exempt_pkg_void;
28 use FS::cust_bill_pkg_fee_void;
33 $me = '[FS::cust_bill_pkg]';
37 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
41 use FS::cust_bill_pkg;
43 $record = new FS::cust_bill_pkg \%hash;
44 $record = new FS::cust_bill_pkg { 'column' => 'value' };
46 $error = $record->insert;
48 $error = $record->check;
52 An FS::cust_bill_pkg object represents an invoice line item.
53 FS::cust_bill_pkg inherits from FS::Record. The following fields are
64 invoice (see L<FS::cust_bill>)
68 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)
70 =item pkgpart_override
72 optional package definition (see L<FS::part_pkg>) override
84 starting date of recurring fee
88 ending date of recurring fee
92 Line item description (overrides normal package description)
96 If not set, defaults to 1
100 If not set, defaults to setup
104 If not set, defaults to recur
108 If set to Y, indicates data should not appear as separate line item on invoice
112 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
113 see L<Time::Local> and L<Date::Parse> for conversion functions.
121 Creates a new line item. To add the line item to the database, see
122 L<"insert">. Line items are normally created by calling the bill method of a
123 customer object (see L<FS::cust_main>).
127 sub table { 'cust_bill_pkg'; }
129 sub detail_table { 'cust_bill_pkg_detail'; }
130 sub display_table { 'cust_bill_pkg_display'; }
131 sub discount_table { 'cust_bill_pkg_discount'; }
132 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
133 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
134 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
138 Adds this line item to the database. If there is an error, returns the error,
139 otherwise returns false.
146 local $SIG{HUP} = 'IGNORE';
147 local $SIG{INT} = 'IGNORE';
148 local $SIG{QUIT} = 'IGNORE';
149 local $SIG{TERM} = 'IGNORE';
150 local $SIG{TSTP} = 'IGNORE';
151 local $SIG{PIPE} = 'IGNORE';
153 my $oldAutoCommit = $FS::UID::AutoCommit;
154 local $FS::UID::AutoCommit = 0;
157 my $error = $self->SUPER::insert;
159 $dbh->rollback if $oldAutoCommit;
163 if ( $self->get('details') ) {
164 foreach my $detail ( @{$self->get('details')} ) {
165 $detail->billpkgnum($self->billpkgnum);
166 $error = $detail->insert;
168 $dbh->rollback if $oldAutoCommit;
169 return "error inserting cust_bill_pkg_detail: $error";
174 if ( $self->get('display') ) {
175 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
176 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
177 $error = $cust_bill_pkg_display->insert;
179 $dbh->rollback if $oldAutoCommit;
180 return "error inserting cust_bill_pkg_display: $error";
185 if ( $self->get('discounts') ) {
186 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
187 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
188 $error = $cust_bill_pkg_discount->insert;
190 $dbh->rollback if $oldAutoCommit;
191 return "error inserting cust_bill_pkg_discount: $error";
196 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
197 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
198 $error = $cust_tax_exempt_pkg->insert;
200 $dbh->rollback if $oldAutoCommit;
201 return "error inserting cust_tax_exempt_pkg: $error";
205 foreach my $tax_link_table (qw(cust_bill_pkg_tax_location
206 cust_bill_pkg_tax_rate_location))
208 my $tax_location = $self->get($tax_link_table) || [];
209 foreach my $link ( @$tax_location ) {
210 my $pkey = $link->primary_key;
211 next if $link->get($pkey); # don't try to double-insert
212 # This cust_bill_pkg can be linked on either side (i.e. it can be the
213 # tax or the taxed item). If the other side is already inserted,
214 # then set billpkgnum to ours, and insert the link. Otherwise,
215 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
216 # on the other side, to be inserted later.
218 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
219 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
220 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
221 # break circular links when doing this
222 $link->set('tax_cust_bill_pkg', '');
224 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
225 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
226 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
227 # XXX pkgnum is zero for tax on tax; it might be better to use
228 # the underlying package?
229 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
230 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
231 $link->set('taxable_cust_bill_pkg', '');
234 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
235 $error = $link->insert;
237 $dbh->rollback if $oldAutoCommit;
238 return "error inserting cust_bill_pkg_tax_location: $error";
241 my $other; # the as yet uninserted cust_bill_pkg
242 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
243 : $link->get('tax_cust_bill_pkg');
244 my $link_array = $other->get( $tax_link_table ) || [];
245 push @$link_array, $link;
246 $other->set( $tax_link_table => $link_array);
251 # someday you will be as awesome as cust_bill_pkg_tax_location...
252 # and today is that day
253 #my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
254 #if ( $tax_rate_location ) {
255 # foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
256 # $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
257 # $error = $cust_bill_pkg_tax_rate_location->insert;
259 # $dbh->rollback if $oldAutoCommit;
260 # return "error inserting cust_bill_pkg_tax_rate_location: $error";
265 my $fee_links = $self->get('cust_bill_pkg_fee');
267 foreach my $link ( @$fee_links ) {
268 # very similar to cust_bill_pkg_tax_location, for obvious reasons
269 next if $link->billpkgfeenum; # don't try to double-insert
271 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
272 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
274 if ( $target and $target->billpkgnum ) {
275 $link->set('billpkgnum', $target->billpkgnum);
276 # base_invnum => null indicates that the fee is based on its own
278 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
279 $link->set('cust_bill_pkg', '');
282 if ( $base and $base->billpkgnum ) {
283 $link->set('base_billpkgnum', $base->billpkgnum);
284 $link->set('base_cust_bill_pkg', '');
286 # it's based on a line item that's not yet inserted
287 my $link_array = $base->get('cust_bill_pkg_fee') || [];
288 push @$link_array, $link;
289 $base->set('cust_bill_pkg_fee' => $link_array);
290 next; # don't insert the link yet
293 $error = $link->insert;
295 $dbh->rollback if $oldAutoCommit;
296 return "error inserting cust_bill_pkg_fee: $error";
301 if ( my $fee_origin = $self->get('fee_origin') ) {
302 $fee_origin->set('billpkgnum' => $self->billpkgnum);
303 $error = $fee_origin->replace;
305 $dbh->rollback if $oldAutoCommit;
306 return "error updating fee origin record: $error";
310 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
311 if ( $cust_tax_adjustment ) {
312 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
313 $error = $cust_tax_adjustment->replace;
315 $dbh->rollback if $oldAutoCommit;
316 return "error replacing cust_tax_adjustment: $error";
320 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
327 Voids this line item: deletes the line item and adds a record of the voided
328 line item to the FS::cust_bill_pkg_void table (and related tables).
334 my $reason = scalar(@_) ? shift : '';
336 local $SIG{HUP} = 'IGNORE';
337 local $SIG{INT} = 'IGNORE';
338 local $SIG{QUIT} = 'IGNORE';
339 local $SIG{TERM} = 'IGNORE';
340 local $SIG{TSTP} = 'IGNORE';
341 local $SIG{PIPE} = 'IGNORE';
343 my $oldAutoCommit = $FS::UID::AutoCommit;
344 local $FS::UID::AutoCommit = 0;
347 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
348 map { $_ => $self->get($_) } $self->fields
350 $cust_bill_pkg_void->reason($reason);
351 my $error = $cust_bill_pkg_void->insert;
353 $dbh->rollback if $oldAutoCommit;
357 foreach my $table (qw(
359 cust_bill_pkg_display
360 cust_bill_pkg_discount
361 cust_bill_pkg_tax_location
362 cust_bill_pkg_tax_rate_location
367 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
369 my $vclass = 'FS::'.$table.'_void';
370 my $void = $vclass->new( {
371 map { $_ => $linked->get($_) } $linked->fields
373 my $error = $void->insert || $linked->delete;
375 $dbh->rollback if $oldAutoCommit;
383 $error = $self->delete;
385 $dbh->rollback if $oldAutoCommit;
389 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
404 local $SIG{HUP} = 'IGNORE';
405 local $SIG{INT} = 'IGNORE';
406 local $SIG{QUIT} = 'IGNORE';
407 local $SIG{TERM} = 'IGNORE';
408 local $SIG{TSTP} = 'IGNORE';
409 local $SIG{PIPE} = 'IGNORE';
411 my $oldAutoCommit = $FS::UID::AutoCommit;
412 local $FS::UID::AutoCommit = 0;
415 foreach my $table (qw(
417 cust_bill_pkg_display
418 cust_bill_pkg_discount
419 cust_bill_pkg_tax_location
420 cust_bill_pkg_tax_rate_location
427 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
428 my $error = $linked->delete;
430 $dbh->rollback if $oldAutoCommit;
437 foreach my $cust_tax_adjustment (
438 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
440 $cust_tax_adjustment->billpkgnum(''); #NULL
441 my $error = $cust_tax_adjustment->replace;
443 $dbh->rollback if $oldAutoCommit;
448 my $error = $self->SUPER::delete(@_);
450 $dbh->rollback if $oldAutoCommit;
454 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
460 #alas, bin/follow-tax-rename
462 #=item replace OLD_RECORD
464 #Currently unimplemented. This would be even more of an accounting nightmare
465 #than deleteing the items. Just don't do it.
470 # return "Can't modify cust_bill_pkg records!";
475 Checks all fields to make sure this is a valid line item. If there is an
476 error, returns the error, otherwise returns false. Called by the insert
485 $self->ut_numbern('billpkgnum')
486 || $self->ut_snumber('pkgnum')
487 || $self->ut_number('invnum')
488 || $self->ut_money('setup')
489 || $self->ut_moneyn('unitsetup')
490 || $self->ut_currencyn('setup_billed_currency')
491 || $self->ut_moneyn('setup_billed_amount')
492 || $self->ut_money('recur')
493 || $self->ut_moneyn('unitrecur')
494 || $self->ut_currencyn('recur_billed_currency')
495 || $self->ut_moneyn('recur_billed_amount')
496 || $self->ut_numbern('sdate')
497 || $self->ut_numbern('edate')
498 || $self->ut_textn('itemdesc')
499 || $self->ut_textn('itemcomment')
500 || $self->ut_enum('hidden', [ '', 'Y' ])
502 return $error if $error;
504 $self->regularize_details;
506 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
507 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
508 return "Unknown pkgnum ". $self->pkgnum
509 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
512 return "Unknown invnum"
513 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
518 =item regularize_details
520 Converts the contents of the 'details' pseudo-field to
521 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
525 sub regularize_details {
527 if ( $self->get('details') ) {
528 foreach my $detail ( @{$self->get('details')} ) {
529 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
530 # then turn it into one
532 if ( ! ref($detail) ) {
533 $hash{'detail'} = $detail;
535 elsif ( ref($detail) eq 'HASH' ) {
538 elsif ( ref($detail) eq 'ARRAY' ) {
539 carp "passing invoice details as arrays is deprecated";
540 #carp "this way sucks, use a hash"; #but more useful/friendly
541 $hash{'format'} = $detail->[0];
542 $hash{'detail'} = $detail->[1];
543 $hash{'amount'} = $detail->[2];
544 $hash{'classnum'} = $detail->[3];
545 $hash{'phonenum'} = $detail->[4];
546 $hash{'accountcode'} = $detail->[5];
547 $hash{'startdate'} = $detail->[6];
548 $hash{'duration'} = $detail->[7];
549 $hash{'regionname'} = $detail->[8];
552 die "unknown detail type ". ref($detail);
554 $detail = new FS::cust_bill_pkg_detail \%hash;
556 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
562 =item set_exemptions TAXOBJECT, OPTIONS
564 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
565 L<FS::tax_rate> record for the tax.
567 This will deal with the following cases:
571 =item Fully exempt customers (cust_main.tax flag) or customer classes
574 =item Customers exempt from specific named taxes (cust_main_exemption
577 =item Taxes that don't apply to setup or recurring fees
578 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
580 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
583 =item Fees that aren't marked as taxable (part_fee.taxable).
587 It does NOT deal with monthly tax exemptions, which need more context
588 than this humble little method cares to deal with.
590 OPTIONS should include "custnum" => the customer number if this tax line
591 hasn't been inserted (which it probably hasn't).
593 Returns a list of exemption objects, which will also be attached to the
594 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
595 item will insert these records as well.
604 my $part_pkg = $self->part_pkg;
605 my $part_fee = $self->part_fee;
608 my $custnum = $opt{custnum};
609 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
611 $cust_main = FS::cust_main->by_key( $custnum )
612 or die "set_exemptions can't identify customer (pass custnum option)\n";
615 my $taxable_charged = $self->setup + $self->recur;
616 return unless $taxable_charged > 0;
618 ### Fully exempt customer ###
620 my $conf = FS::Conf->new;
621 if ( $conf->exists('cust_class-tax_exempt') ) {
622 my $cust_class = $cust_main->cust_class;
623 $exempt_cust = $cust_class->tax if $cust_class;
625 $exempt_cust = $cust_main->tax;
628 ### Exemption from named tax ###
629 my $exempt_cust_taxname;
630 if ( !$exempt_cust and $tax->taxname ) {
631 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
634 if ( $exempt_cust ) {
636 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
637 amount => $taxable_charged,
640 $taxable_charged = 0;
642 } elsif ( $exempt_cust_taxname ) {
644 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
645 amount => $taxable_charged,
646 exempt_cust_taxname => 'Y',
648 $taxable_charged = 0;
652 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
653 or ($part_pkg and $part_pkg->setuptax)
658 and $taxable_charged > 0 ) {
660 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
661 amount => $self->setup,
664 $taxable_charged -= $self->setup;
668 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
669 or ($part_pkg and $part_pkg->recurtax)
674 and $taxable_charged > 0 ) {
676 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
677 amount => $self->recur,
680 $taxable_charged -= $self->recur;
684 foreach (@new_exemptions) {
685 $_->set('taxnum', $tax->taxnum);
686 $_->set('taxtype', ref($tax));
689 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
690 return @new_exemptions;
696 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
700 Returns the customer (L<FS::cust_main> object) for this line item.
705 # required for cust_main_Mixin equivalence
706 # and use cust_bill instead of cust_pkg because this might not have a
709 my $cust_bill = $self->cust_bill or return '';
710 $cust_bill->cust_main;
713 =item previous_cust_bill_pkg
715 Returns the previous cust_bill_pkg for this package, if any.
719 sub previous_cust_bill_pkg {
721 return unless $self->sdate;
723 'table' => 'cust_bill_pkg',
724 'hashref' => { 'pkgnum' => $self->pkgnum,
725 'sdate' => { op=>'<', value=>$self->sdate },
727 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
733 Returns the amount owed (still outstanding) on this line item's setup fee,
734 which is the amount of the line item minus all payment applications (see
735 L<FS::cust_bill_pay_pkg> and credit applications (see
736 L<FS::cust_credit_bill_pkg>).
742 $self->owed('setup', @_);
747 Returns the amount owed (still outstanding) on this line item's recurring fee,
748 which is the amount of the line item minus all payment applications (see
749 L<FS::cust_bill_pay_pkg> and credit applications (see
750 L<FS::cust_credit_bill_pkg>).
756 $self->owed('recur', @_);
759 # modeled after cust_bill::owed...
761 my( $self, $field ) = @_;
762 my $balance = $self->$field();
763 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
764 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
765 $balance = sprintf( '%.2f', $balance );
766 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
772 my( $self, $field ) = @_;
773 my $balance = $self->$field();
774 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
775 $balance = sprintf( '%.2f', $balance );
776 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
780 sub cust_bill_pay_pkg {
781 my( $self, $field ) = @_;
782 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
783 'setuprecur' => $field,
788 sub cust_credit_bill_pkg {
789 my( $self, $field ) = @_;
790 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
791 'setuprecur' => $field,
798 Returns the number of billing units (for tax purposes) represented by this,
805 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
810 If this item has any discounts, returns a hashref in the format used
811 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
812 on an invoice. This will contain the keys 'description', 'amount',
813 'ext_description' (an arrayref of text lines describing the discounts),
814 and '_is_discount' (a flag).
816 The value for 'amount' will be negative, and will be scaled for the package
823 my @pkg_discounts = $self->pkg_discount;
824 return if @pkg_discounts == 0;
825 # special case: if there are old "discount details" on this line item, don't
826 # show discount line items
827 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
834 description => $self->mt('Discount'),
836 ext_description => \@ext,
837 pkgpart => $self->pkgpart,
838 feepart => $self->feepart,
839 # maybe should show quantity/unit discount?
841 foreach my $pkg_discount (@pkg_discounts) {
842 push @ext, $pkg_discount->description;
843 $d->{amount} -= $pkg_discount->amount;
845 $d->{amount} *= $self->quantity || 1;
850 =item set_display OPTION => VALUE ...
852 A helper method for I<insert>, populates the pseudo-field B<display> with
853 appropriate FS::cust_bill_pkg_display objects.
855 Options are passed as a list of name/value pairs. Options are:
857 part_pkg: FS::part_pkg object from this line item's package.
859 real_pkgpart: if this line item comes from a bundled package, the pkgpart
860 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
865 my( $self, %opt ) = @_;
866 my $part_pkg = $opt{'part_pkg'};
867 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
869 my $conf = new FS::Conf;
871 # whether to break this down into setup/recur/usage
872 my $separate = $conf->exists('separate_usage');
874 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
875 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
877 # or use the category from $opt{'part_pkg'} if its not bundled?
878 my $categoryname = $cust_pkg->part_pkg->categoryname;
880 # if we don't have to separate setup/recur/usage, or put this in a
881 # package-specific section, or display a usage summary, then don't
882 # even create one of these. The item will just display in the unnamed
883 # section as a single line plus details.
884 return $self->set('display', [])
885 unless $separate || $categoryname || $usage_mandate;
889 my %hash = ( 'section' => $categoryname );
891 # whether to put usage details in a separate section, and if so, which one
892 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
893 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
895 # whether to show a usage summary line (total usage charges, no details)
896 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
897 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
900 # create lines for setup and (non-usage) recur, in the main section
901 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
902 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
904 # display everything in a single line
905 push @display, new FS::cust_bill_pkg_display
908 # and if usage_mandate is enabled, hide details
909 # (this only works on multisection invoices...)
910 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
914 if ($separate && $usage_section && $summary) {
915 # create a line for the usage summary in the main section
916 push @display, new FS::cust_bill_pkg_display { type => 'U',
922 if ($usage_mandate || ($usage_section && $summary) ) {
923 $hash{post_total} = 'Y';
926 if ($separate || $usage_mandate) {
927 # show call details for this line item in the usage section.
928 # if usage_mandate is on, this will display below the section subtotal.
929 # this also happens if usage is in a separate section and there's a
930 # summary in the main section, though I'm not sure why.
931 $hash{section} = $usage_section if $usage_section;
932 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
935 $self->set('display', \@display);
941 Returns a hash: keys are "setup", "recur" or usage classnum, values are
942 FS::cust_bill_pkg objects, each with no more than a single class (setup or
949 # XXX this goes away with cust_bill_pkg refactor
950 # or at least I wish it would, but it turns out to be harder than
953 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
954 my %cust_bill_pkg = ();
957 foreach my $classnum ($self->usage_classes) {
958 my $amount = $self->usage($classnum);
959 next if $amount == 0; # though if so we shouldn't be here
960 my $usage_item = FS::cust_bill_pkg->new({
964 'taxclass' => $classnum,
967 $cust_bill_pkg{$classnum} = $usage_item;
968 $usage_total += $amount;
971 foreach (qw(setup recur)) {
972 next if ($self->get($_) == 0);
973 my $item = FS::cust_bill_pkg->new({
980 $item->set($_, $self->get($_));
981 $cust_bill_pkg{$_} = $item;
985 $cust_bill_pkg{recur}->set('recur',
986 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
995 Returns the amount of the charge associated with usage class CLASSNUM if
996 CLASSNUM is defined. Otherwise returns the total charge associated with
1002 my( $self, $classnum ) = @_;
1003 $self->regularize_details;
1005 if ( $self->get('details') ) {
1008 map { $_->amount || 0 }
1009 grep { !defined($classnum) or $classnum eq $_->classnum }
1010 @{ $self->get('details') }
1015 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1016 ' WHERE billpkgnum = '. $self->billpkgnum;
1017 if (defined $classnum) {
1018 if ($classnum =~ /^(\d+)$/) {
1019 $sql .= " AND classnum = $1";
1020 } elsif ($classnum eq '') {
1021 $sql .= " AND classnum IS NULL";
1025 my $sth = dbh->prepare($sql) or die dbh->errstr;
1026 $sth->execute or die $sth->errstr;
1028 return $sth->fetchrow_arrayref->[0] || 0;
1036 Returns a list of usage classnums associated with this invoice line's
1043 $self->regularize_details;
1045 if ( $self->get('details') ) {
1047 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1052 map { $_->classnum }
1053 qsearch({ table => 'cust_bill_pkg_detail',
1054 hashref => { billpkgnum => $self->billpkgnum },
1055 select => 'DISTINCT classnum',
1062 sub cust_tax_exempt_pkg {
1065 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1068 =item cust_bill_pkg_tax_Xlocation
1070 Returns the list of associated cust_bill_pkg_tax_location and/or
1071 cust_bill_pkg_tax_rate_location objects
1075 sub cust_bill_pkg_tax_Xlocation {
1078 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1081 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1082 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1087 =item recur_show_zero
1091 sub recur_show_zero { shift->_X_show_zero('recur'); }
1092 sub setup_show_zero { shift->_X_show_zero('setup'); }
1095 my( $self, $what ) = @_;
1097 return 0 unless $self->$what() == 0 && $self->pkgnum;
1099 $self->cust_pkg->_X_show_zero($what);
1102 =item credited [ BEFORE, AFTER, OPTIONS ]
1104 Returns the sum of credits applied to this item. Arguments are the same as
1105 owed_sql/paid_sql/credited_sql.
1111 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1114 =item tax_locationnum
1116 Returns the L<FS::cust_location> number that this line item is in for tax
1117 purposes. For package sales, it's the package tax location; for fees,
1118 it's the customer's default service location.
1122 sub tax_locationnum {
1124 if ( $self->pkgnum ) { # normal sales
1125 return $self->cust_pkg->tax_locationnum;
1126 } elsif ( $self->feepart ) { # fees
1127 my $custnum = $self->fee_origin->custnum;
1129 return FS::cust_main->by_key($custnum)->ship_locationnum;
1138 if ( $self->pkgnum ) { # normal sales
1139 return $self->cust_pkg->tax_location;
1140 } elsif ( $self->feepart ) { # fees
1141 my $custnum = $self->fee_origin->custnum;
1143 return FS::cust_main->by_key($custnum)->ship_location;
1152 =head1 CLASS METHODS
1158 Returns an SQL expression for the total usage charges in details on
1164 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1165 FROM cust_bill_pkg_detail
1166 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1168 sub usage_sql { $usage_sql }
1170 # this makes owed_sql, etc. much more concise
1172 my ($class, $start, $end, %opt) = @_;
1173 my $setuprecur = $opt{setuprecur} || '';
1175 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1176 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1177 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1179 if ($opt{no_usage} and $charged =~ /recur/) {
1180 $charged = "$charged - $usage_sql"
1187 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1189 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1190 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1191 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1197 '(' . $class->charged_sql(@_) .
1198 ' - ' . $class->paid_sql(@_) .
1199 ' - ' . $class->credited_sql(@_) . ')'
1202 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1204 Returns an SQL expression for the sum of payments applied to this item.
1209 my ($class, $start, $end, %opt) = @_;
1210 my $s = $start ? "AND cust_pay._date <= $start" : '';
1211 my $e = $end ? "AND cust_pay._date > $end" : '';
1212 my $setuprecur = $opt{setuprecur} || '';
1213 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1214 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1215 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1217 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1218 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1219 JOIN cust_pay USING (paynum)
1220 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1221 $s $e $setuprecur )";
1223 if ( $opt{no_usage} ) {
1224 # cap the amount paid at the sum of non-usage charges,
1225 # minus the amount credited against non-usage charges
1227 $class->charged_sql($start, $end, %opt) . ' - ' .
1228 $class->credited_sql($start, $end, %opt).')';
1237 my ($class, $start, $end, %opt) = @_;
1238 my $s = $start ? "AND cust_credit._date <= $start" : '';
1239 my $e = $end ? "AND cust_credit._date > $end" : '';
1240 my $setuprecur = $opt{setuprecur} || '';
1241 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1242 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1243 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1245 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1246 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1247 JOIN cust_credit USING (crednum)
1248 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1249 $s $e $setuprecur )";
1251 if ( $opt{no_usage} ) {
1252 # cap the amount credited at the sum of non-usage charges
1253 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1261 sub upgrade_tax_location {
1262 # For taxes that were calculated/invoiced before cust_location refactoring
1263 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1264 # they were calculated on a package-location basis. Create them here,
1265 # along with any necessary cust_location records and any tax exemption
1268 my ($class, %opt) = @_;
1269 # %opt may include 's' and 'e': start and end date ranges
1270 # and 'X': abort on any error, instead of just rolling back changes to
1273 my $oldAutoCommit = $FS::UID::AutoCommit;
1274 local $FS::UID::AutoCommit = 0;
1277 use FS::h_cust_main;
1278 use FS::h_cust_bill;
1280 use FS::h_cust_main_exemption;
1283 local $FS::cust_location::import = 1;
1285 my $conf = FS::Conf->new; # h_conf?
1286 return if $conf->config('tax_data_vendor'); #don't touch this case
1287 my $use_ship = $conf->exists('tax-ship_address');
1288 my $use_pkgloc = $conf->exists('tax-pkg_address');
1290 my $date_where = '';
1292 $date_where .= " AND cust_bill._date >= $opt{s}";
1295 $date_where .= " AND cust_bill._date < $opt{e}";
1298 my $commit_each_invoice = 1 unless $opt{X};
1300 # if an invoice has either of these kinds of objects, then it doesn't
1301 # need to be upgraded...probably
1302 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1303 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1304 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1305 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1306 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1307 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1308 ' AND exempt_monthly IS NULL';
1310 my %all_tax_names = (
1313 map { $_->taxname => 1 }
1314 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1317 my $search = FS::Cursor->new({
1318 table => 'cust_bill',
1320 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1321 "AND NOT EXISTS($sub_has_exempt) ".
1325 #print "Processing ".scalar(@invnums)." invoices...\n";
1329 while (my $cust_bill = $search->fetch) {
1330 my $invnum = $cust_bill->invnum;
1332 print STDERR "Invoice #$invnum\n";
1334 my %pkgpart_taxclass; # pkgpart => taxclass
1335 my %pkgpart_exempt_setup;
1336 my %pkgpart_exempt_recur;
1337 my $h_cust_bill = qsearchs('h_cust_bill',
1338 { invnum => $invnum,
1339 history_action => 'insert' });
1340 if (!$h_cust_bill) {
1341 warn "no insert record for invoice $invnum; skipped\n";
1342 #$date = $cust_bill->_date as a fallback?
1343 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1344 # when looking up history records in other tables.
1347 my $custnum = $h_cust_bill->custnum;
1349 # Determine the address corresponding to this tax region.
1350 # It's either the bill or ship address of the customer as of the
1351 # invoice date-of-insertion. (Not necessarily the invoice date.)
1352 my $date = $h_cust_bill->history_date;
1353 local($FS::Record::qsearch_qualify_columns) = 0;
1354 my $h_cust_main = qsearchs('h_cust_main',
1355 { custnum => $custnum },
1356 FS::h_cust_main->sql_h_searchs($date)
1358 if (!$h_cust_main ) {
1359 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1361 # fallback to current $cust_main? sounds dangerous.
1364 # This is a historical customer record, so it has a historical address.
1365 # If there's no cust_location matching this custnum and address (there
1366 # probably isn't), create one.
1367 my %tax_loc; # keys are pkgnums, values are cust_location objects
1368 my $default_tax_loc;
1369 if ( $h_cust_main->bill_locationnum ) {
1370 # the location has already been upgraded
1372 $default_tax_loc = $h_cust_main->ship_location;
1374 $default_tax_loc = $h_cust_main->bill_location;
1377 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1378 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1379 FS::cust_main->location_fields;
1380 # not really needed for this, and often result in duplicate locations
1381 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1383 $hash{custnum} = $h_cust_main->custnum;
1384 $default_tax_loc = FS::cust_location->new(\%hash);
1385 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1387 warn "couldn't create historical location record for cust#".
1388 $h_cust_main->custnum.": $error\n";
1393 $exempt_cust = 1 if $h_cust_main->tax;
1395 # classify line items
1397 my %nontax_items; # taxclass => array of cust_bill_pkg
1398 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1399 my $pkgnum = $item->pkgnum;
1401 if ( $pkgnum == 0 ) {
1403 push @tax_items, $item;
1406 # (pkgparts really shouldn't change, right?)
1407 local($FS::Record::qsearch_qualify_columns) = 0;
1408 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1409 FS::h_cust_pkg->sql_h_searchs($date)
1411 if ( !$h_cust_pkg ) {
1412 warn "no historical package #".$item->pkgpart."; skipped\n";
1415 my $pkgpart = $h_cust_pkg->pkgpart;
1417 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1418 # then this package already had a locationnum assigned, and that's
1419 # the one to use for tax calculation
1420 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1422 # use the customer's bill or ship loc, which was inserted earlier
1423 $tax_loc{$pkgnum} = $default_tax_loc;
1426 if (!exists $pkgpart_taxclass{$pkgpart}) {
1427 local($FS::Record::qsearch_qualify_columns) = 0;
1428 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1429 FS::h_part_pkg->sql_h_searchs($date)
1431 if ( !$h_part_pkg ) {
1432 warn "no historical package def #$pkgpart; skipped\n";
1435 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1436 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1437 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1440 # mark any exemptions that apply
1441 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1442 $item->set('exempt_setup' => 1);
1445 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1446 $item->set('exempt_recur' => 1);
1449 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1451 $nontax_items{$taxclass} ||= [];
1452 push @{ $nontax_items{$taxclass} }, $item;
1456 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1459 # Get any per-customer taxname exemptions that were in effect.
1460 my %exempt_cust_taxname;
1461 foreach (keys %all_tax_names) {
1462 local($FS::Record::qsearch_qualify_columns) = 0;
1463 my $h_exemption = qsearchs('h_cust_main_exemption', {
1464 'custnum' => $custnum,
1467 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1470 $exempt_cust_taxname{ $_ } = 1;
1474 # Use a variation on the procedure in
1475 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1477 my @loc_keys = qw( district city county state country );
1478 my %taxdef_by_name; # by name, and then by taxclass
1479 my %est_tax; # by name, and then by taxclass
1480 my %taxable_items; # by taxnum, and then an array
1482 foreach my $taxclass (keys %nontax_items) {
1483 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1484 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1485 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1486 my @elim = qw( district city county state );
1487 my @taxdefs; # because there may be several with different taxnames
1489 $myhash{taxclass} = $taxclass;
1490 @taxdefs = qsearch('cust_main_county', \%myhash);
1492 $myhash{taxclass} = '';
1493 @taxdefs = qsearch('cust_main_county', \%myhash);
1495 $myhash{ shift @elim } = '';
1496 } while scalar(@elim) and !@taxdefs;
1498 foreach my $taxdef (@taxdefs) {
1499 next if $taxdef->tax == 0;
1500 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1502 $taxable_items{$taxdef->taxnum} ||= [];
1503 # clone the item so that taxdef-dependent changes don't
1504 # change it for other taxdefs
1505 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1507 # these flags are already set if the part_pkg declares itself exempt
1508 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1509 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1512 my $taxable = $item->setup + $item->recur;
1514 # h_cust_credit_bill_pkg?
1515 # NO. Because if these exemptions HAD been created at the time of
1516 # billing, and then a credit applied later, the exemption would
1517 # have been adjusted by the amount of the credit. So we adjust
1518 # the taxable amount before creating the exemption.
1519 # But don't deduct the credit from taxable, because the tax was
1520 # calculated before the credit was applied.
1521 foreach my $f (qw(setup recur)) {
1522 my $credited = FS::Record->scalar_sql(
1523 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1524 "WHERE billpkgnum = ? AND setuprecur = ?",
1528 $item->set($f, $item->get($f) - $credited) if $credited;
1530 my $existing_exempt = FS::Record->scalar_sql(
1531 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1532 "billpkgnum = ? AND taxnum = ?",
1533 $item->billpkgnum, $taxdef->taxnum
1535 $taxable -= $existing_exempt;
1537 if ( $taxable and $exempt_cust ) {
1538 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1541 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1542 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1545 if ( $taxable and $item->exempt_setup ) {
1546 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1547 $taxable -= $item->setup;
1549 if ( $taxable and $item->exempt_recur ) {
1550 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1551 $taxable -= $item->recur;
1554 $item->set('taxable' => $taxable);
1555 push @{ $taxable_items{$taxdef->taxnum} }, $item
1558 # estimate the amount of tax (this is necessary because different
1559 # taxdefs with the same taxname may have different tax rates)
1560 # and sum that for each taxname/taxclass combination
1562 $est_tax{$taxdef->taxname} ||= {};
1563 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1564 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1565 $taxable * $taxdef->tax;
1567 foreach (@new_exempt) {
1568 next if $_->{amount} == 0;
1569 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1571 billpkgnum => $item->billpkgnum,
1572 taxnum => $taxdef->taxnum,
1574 my $error = $cust_tax_exempt_pkg->insert;
1576 my $pkgnum = $item->pkgnum;
1577 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1581 } #foreach @new_exempt
1584 } #foreach $taxclass
1586 # Now go through the billed taxes and match them up with the line items.
1587 TAX_ITEM: foreach my $tax_item ( @tax_items )
1589 my $taxname = $tax_item->itemdesc;
1590 $taxname = '' if $taxname eq 'Tax';
1592 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1593 # then we didn't find any applicable taxes with this name
1594 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1595 # possibly all of these should be "next TAX_ITEM", but whole invoices
1596 # are transaction protected and we can go back and retry them.
1599 # classname => cust_main_county
1600 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1602 # Divide the tax item among taxclasses, if necessary
1603 # classname => estimated tax amount
1604 my $this_est_tax = $est_tax{$taxname};
1605 if (!defined $this_est_tax) {
1606 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1609 my $est_total = sum(values %$this_est_tax);
1610 if ( $est_total == 0 ) {
1612 warn "estimated tax on invoice #$invnum is zero.\n";
1616 my $real_tax = $tax_item->setup;
1617 printf ("Distributing \$%.2f tax:\n", $real_tax);
1618 my $cents_remaining = $real_tax * 100; # for rounding error
1619 my @tax_links; # partial CBPTL hashrefs
1620 foreach my $taxclass (keys %taxdef_by_class) {
1621 my $taxdef = $taxdef_by_class{$taxclass};
1622 # these items already have "taxable" set to their charge amount
1623 # after applying any credits or exemptions
1624 my @items = @{ $taxable_items{$taxdef->taxnum} };
1625 my $subtotal = sum(map {$_->get('taxable')} @items);
1626 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1628 foreach my $nontax (@items) {
1629 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1630 my $part = int($real_tax
1632 * ($this_est_tax->{$taxclass}/$est_total)
1634 * ($nontax->get('taxable'))/$subtotal
1638 $cents_remaining -= $part;
1640 taxnum => $taxdef->taxnum,
1641 pkgnum => $nontax->pkgnum,
1642 locationnum => $my_tax_loc->locationnum,
1643 billpkgnum => $nontax->billpkgnum,
1647 } #foreach $taxclass
1648 # Distribute any leftover tax round-robin style, one cent at a time.
1650 my $nlinks = scalar(@tax_links);
1652 # ensure that it really is an integer
1653 $cents_remaining = sprintf('%.0f', $cents_remaining);
1654 while ($cents_remaining > 0) {
1655 $tax_links[$i % $nlinks]->{cents} += 1;
1660 warn "Can't create tax links--no taxable items found.\n";
1664 # Gather credit/payment applications so that we can link them
1667 qsearch( 'cust_credit_bill_pkg',
1668 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1670 qsearch( 'cust_bill_pay_pkg',
1671 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1675 # grab the first one
1676 my $this_unlinked = shift @unlinked;
1677 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1679 # Create tax links (yay!)
1680 printf("Creating %d tax links.\n",scalar(@tax_links));
1681 foreach (@tax_links) {
1682 my $link = FS::cust_bill_pkg_tax_location->new({
1683 billpkgnum => $tax_item->billpkgnum,
1684 taxtype => 'FS::cust_main_county',
1685 locationnum => $_->{locationnum},
1686 taxnum => $_->{taxnum},
1687 pkgnum => $_->{pkgnum},
1688 amount => sprintf('%.2f', $_->{cents} / 100),
1689 taxable_billpkgnum => $_->{billpkgnum},
1691 my $error = $link->insert;
1693 warn "Can't create tax link for inv#$invnum: $error\n";
1697 my $link_cents = $_->{cents};
1698 # update/create subitem links
1700 # If $this_unlinked is undef, then we've allocated all of the
1701 # credit/payment applications to the tax item. If $link_cents is 0,
1702 # then we've applied credits/payments to all of this package fraction,
1703 # so go on to the next.
1704 while ($this_unlinked and $link_cents) {
1705 # apply as much as possible of $link_amount to this credit/payment
1707 my $apply_cents = min($link_cents, $unlinked_cents);
1708 $link_cents -= $apply_cents;
1709 $unlinked_cents -= $apply_cents;
1710 # $link_cents or $unlinked_cents or both are now zero
1711 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1712 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1713 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1714 if ( $this_unlinked->$pkey ) {
1715 # then it's an existing link--replace it
1716 $error = $this_unlinked->replace;
1718 $this_unlinked->insert;
1720 # what do we do with errors at this stage?
1722 warn "Error creating tax application link: $error\n";
1723 next INVOICE; # for lack of a better idea
1726 if ( $unlinked_cents == 0 ) {
1727 # then we've allocated all of this payment/credit application,
1728 # so grab the next one
1729 $this_unlinked = shift @unlinked;
1730 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1731 } elsif ( $link_cents == 0 ) {
1732 # then we've covered all of this package tax fraction, so split
1733 # off a new application from this one
1734 $this_unlinked = $this_unlinked->new({
1735 $this_unlinked->hash,
1738 # $unlinked_cents is still what it is
1741 } #while $this_unlinked and $link_cents
1742 } #foreach (@tax_links)
1743 } #foreach $tax_item
1745 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1751 $dbh->rollback if $oldAutoCommit;
1752 die "Upgrade halted.\n" unless $commit_each_invoice;
1756 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1761 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1765 use Date::Parse 'str2time';
1768 my $upgrade = 'tax_location_2012';
1769 return if FS::upgrade_journal->is_done($upgrade);
1770 my $job = FS::queue->new({
1771 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1773 # call it kind of like a class method, not that it matters much
1774 $job->insert($class, 's' => str2time('2012-01-01'));
1775 # if there's a customer location upgrade queued also, wait for it to
1777 my $location_job = qsearchs('queue', {
1778 job => 'FS::cust_main::Location::process_upgrade_location'
1780 if ( $location_job ) {
1781 $job->depend_insert($location_job->jobnum);
1783 # Then mark the upgrade as done, so that we don't queue the job twice
1784 # and somehow run two of them concurrently.
1785 FS::upgrade_journal->set_done($upgrade);
1786 # This upgrade now does the job of assigning taxable_billpkgnums to
1787 # cust_bill_pkg_tax_location, so set that task done also.
1788 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1795 setup and recur shouldn't be separate fields. There should be one "amount"
1796 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1798 A line item with both should really be two separate records (preserving
1799 sdate and edate for setup fees for recurring packages - that information may
1800 be valuable later). Invoice generation (cust_main::bill), invoice printing
1801 (cust_bill), tax reports (report_tax.cgi) and line item reports
1802 (cust_bill_pkg.cgi) would need to be updated.
1804 owed_setup and owed_recur could then be repaced by just owed, and
1805 cust_bill::open_cust_bill_pkg and
1806 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1808 The upgrade procedure is pretty sketchy.
1812 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1813 from the base documentation.