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;
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";
243 my $other; # the as yet uninserted cust_bill_pkg
244 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
245 : $link->get('tax_cust_bill_pkg');
246 my $link_array = $other->get( $tax_link_table ) || [];
247 push @$link_array, $link;
248 $other->set( $tax_link_table => $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;
327 =item void [ REASON ]
329 Voids this line item: deletes the line item and adds a record of the voided
330 line item to the FS::cust_bill_pkg_void table (and related tables).
336 my $reason = scalar(@_) ? shift : '';
338 unless (ref($reason) || !$reason) {
339 $reason = FS::reason->new_or_existing(
341 'type' => 'Invoice void',
346 local $SIG{HUP} = 'IGNORE';
347 local $SIG{INT} = 'IGNORE';
348 local $SIG{QUIT} = 'IGNORE';
349 local $SIG{TERM} = 'IGNORE';
350 local $SIG{TSTP} = 'IGNORE';
351 local $SIG{PIPE} = 'IGNORE';
353 my $oldAutoCommit = $FS::UID::AutoCommit;
354 local $FS::UID::AutoCommit = 0;
357 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
358 map { $_ => $self->get($_) } $self->fields
360 $cust_bill_pkg_void->reasonnum($reason->reasonnum) if $reason;
361 my $error = $cust_bill_pkg_void->insert;
363 $dbh->rollback if $oldAutoCommit;
367 foreach my $table (qw(
369 cust_bill_pkg_display
370 cust_bill_pkg_discount
371 cust_bill_pkg_tax_location
372 cust_bill_pkg_tax_rate_location
377 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
379 my $vclass = 'FS::'.$table.'_void';
380 my $void = $vclass->new( {
381 map { $_ => $linked->get($_) } $linked->fields
383 my $error = $void->insert || $linked->delete;
385 $dbh->rollback if $oldAutoCommit;
393 $error = $self->delete;
395 $dbh->rollback if $oldAutoCommit;
399 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
414 local $SIG{HUP} = 'IGNORE';
415 local $SIG{INT} = 'IGNORE';
416 local $SIG{QUIT} = 'IGNORE';
417 local $SIG{TERM} = 'IGNORE';
418 local $SIG{TSTP} = 'IGNORE';
419 local $SIG{PIPE} = 'IGNORE';
421 my $oldAutoCommit = $FS::UID::AutoCommit;
422 local $FS::UID::AutoCommit = 0;
425 foreach my $table (qw(
427 cust_bill_pkg_display
428 cust_bill_pkg_discount
429 cust_bill_pkg_tax_location
430 cust_bill_pkg_tax_rate_location
437 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
438 my $error = $linked->delete;
440 $dbh->rollback if $oldAutoCommit;
447 foreach my $cust_tax_adjustment (
448 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
450 $cust_tax_adjustment->billpkgnum(''); #NULL
451 my $error = $cust_tax_adjustment->replace;
453 $dbh->rollback if $oldAutoCommit;
458 my $error = $self->SUPER::delete(@_);
460 $dbh->rollback if $oldAutoCommit;
464 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
470 #alas, bin/follow-tax-rename
472 #=item replace OLD_RECORD
474 #Currently unimplemented. This would be even more of an accounting nightmare
475 #than deleteing the items. Just don't do it.
480 # return "Can't modify cust_bill_pkg records!";
485 Checks all fields to make sure this is a valid line item. If there is an
486 error, returns the error, otherwise returns false. Called by the insert
495 $self->ut_numbern('billpkgnum')
496 || $self->ut_snumber('pkgnum')
497 || $self->ut_number('invnum')
498 || $self->ut_money('setup')
499 || $self->ut_moneyn('unitsetup')
500 || $self->ut_currencyn('setup_billed_currency')
501 || $self->ut_moneyn('setup_billed_amount')
502 || $self->ut_money('recur')
503 || $self->ut_moneyn('unitrecur')
504 || $self->ut_currencyn('recur_billed_currency')
505 || $self->ut_moneyn('recur_billed_amount')
506 || $self->ut_numbern('sdate')
507 || $self->ut_numbern('edate')
508 || $self->ut_textn('itemdesc')
509 || $self->ut_textn('itemcomment')
510 || $self->ut_enum('hidden', [ '', 'Y' ])
512 return $error if $error;
514 $self->regularize_details;
516 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
517 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
518 return "Unknown pkgnum ". $self->pkgnum
519 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
522 return "Unknown invnum"
523 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
528 =item regularize_details
530 Converts the contents of the 'details' pseudo-field to
531 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
535 sub regularize_details {
537 if ( $self->get('details') ) {
538 foreach my $detail ( @{$self->get('details')} ) {
539 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
540 # then turn it into one
542 if ( ! ref($detail) ) {
543 $hash{'detail'} = $detail;
545 elsif ( ref($detail) eq 'HASH' ) {
548 elsif ( ref($detail) eq 'ARRAY' ) {
549 carp "passing invoice details as arrays is deprecated";
550 #carp "this way sucks, use a hash"; #but more useful/friendly
551 $hash{'format'} = $detail->[0];
552 $hash{'detail'} = $detail->[1];
553 $hash{'amount'} = $detail->[2];
554 $hash{'classnum'} = $detail->[3];
555 $hash{'phonenum'} = $detail->[4];
556 $hash{'accountcode'} = $detail->[5];
557 $hash{'startdate'} = $detail->[6];
558 $hash{'duration'} = $detail->[7];
559 $hash{'regionname'} = $detail->[8];
562 die "unknown detail type ". ref($detail);
564 $detail = new FS::cust_bill_pkg_detail \%hash;
566 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
572 =item set_exemptions TAXOBJECT, OPTIONS
574 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
575 L<FS::tax_rate> record for the tax.
577 This will deal with the following cases:
581 =item Fully exempt customers (cust_main.tax flag) or customer classes
584 =item Customers exempt from specific named taxes (cust_main_exemption
587 =item Taxes that don't apply to setup or recurring fees
588 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
590 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
593 =item Fees that aren't marked as taxable (part_fee.taxable).
597 It does NOT deal with monthly tax exemptions, which need more context
598 than this humble little method cares to deal with.
600 OPTIONS should include "custnum" => the customer number if this tax line
601 hasn't been inserted (which it probably hasn't).
603 Returns a list of exemption objects, which will also be attached to the
604 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
605 item will insert these records as well.
614 my $part_pkg = $self->part_pkg;
615 my $part_fee = $self->part_fee;
618 my $custnum = $opt{custnum};
619 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
621 $cust_main = FS::cust_main->by_key( $custnum )
622 or die "set_exemptions can't identify customer (pass custnum option)\n";
625 my $taxable_charged = $self->setup + $self->recur;
626 return unless $taxable_charged > 0;
628 ### Fully exempt customer ###
630 my $conf = FS::Conf->new;
631 if ( $conf->exists('cust_class-tax_exempt') ) {
632 my $cust_class = $cust_main->cust_class;
633 $exempt_cust = $cust_class->tax if $cust_class;
635 $exempt_cust = $cust_main->tax;
638 ### Exemption from named tax ###
639 my $exempt_cust_taxname;
640 if ( !$exempt_cust and $tax->taxname ) {
641 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
644 if ( $exempt_cust ) {
646 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
647 amount => $taxable_charged,
650 $taxable_charged = 0;
652 } elsif ( $exempt_cust_taxname ) {
654 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
655 amount => $taxable_charged,
656 exempt_cust_taxname => 'Y',
658 $taxable_charged = 0;
662 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
663 or ($part_pkg and $part_pkg->setuptax)
668 and $taxable_charged > 0 ) {
670 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
671 amount => $self->setup,
674 $taxable_charged -= $self->setup;
678 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
679 or ($part_pkg and $part_pkg->recurtax)
684 and $taxable_charged > 0 ) {
686 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
687 amount => $self->recur,
690 $taxable_charged -= $self->recur;
694 foreach (@new_exemptions) {
695 $_->set('taxnum', $tax->taxnum);
696 $_->set('taxtype', ref($tax));
699 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
700 return @new_exemptions;
706 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
710 Returns the customer (L<FS::cust_main> object) for this line item.
715 # required for cust_main_Mixin equivalence
716 # and use cust_bill instead of cust_pkg because this might not have a
719 my $cust_bill = $self->cust_bill or return '';
720 $cust_bill->cust_main;
723 =item previous_cust_bill_pkg
725 Returns the previous cust_bill_pkg for this package, if any.
729 sub previous_cust_bill_pkg {
731 return unless $self->sdate;
733 'table' => 'cust_bill_pkg',
734 'hashref' => { 'pkgnum' => $self->pkgnum,
735 'sdate' => { op=>'<', value=>$self->sdate },
737 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
743 Returns the amount owed (still outstanding) on this line item's setup fee,
744 which is the amount of the line item minus all payment applications (see
745 L<FS::cust_bill_pay_pkg> and credit applications (see
746 L<FS::cust_credit_bill_pkg>).
752 $self->owed('setup', @_);
757 Returns the amount owed (still outstanding) on this line item's recurring fee,
758 which is the amount of the line item minus all payment applications (see
759 L<FS::cust_bill_pay_pkg> and credit applications (see
760 L<FS::cust_credit_bill_pkg>).
766 $self->owed('recur', @_);
769 # modeled after cust_bill::owed...
771 my( $self, $field ) = @_;
772 my $balance = $self->$field();
773 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($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
782 my( $self, $field ) = @_;
783 my $balance = $self->$field();
784 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
785 $balance = sprintf( '%.2f', $balance );
786 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
790 sub cust_bill_pay_pkg {
791 my( $self, $field ) = @_;
792 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
793 'setuprecur' => $field,
798 sub cust_credit_bill_pkg {
799 my( $self, $field ) = @_;
800 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
801 'setuprecur' => $field,
808 Returns the number of billing units (for tax purposes) represented by this,
815 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
820 If this item has any discounts, returns a hashref in the format used
821 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
822 on an invoice. This will contain the keys 'description', 'amount',
823 'ext_description' (an arrayref of text lines describing the discounts),
824 and '_is_discount' (a flag).
826 The value for 'amount' will be negative, and will be scaled for the package
835 my @pkg_discounts = $self->pkg_discount;
836 return if @pkg_discounts == 0;
837 # special case: if there are old "discount details" on this line item, don't
838 # show discount line items
839 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
846 description => $self->mt('Discount'),
849 ext_description => \@ext,
850 pkgpart => $self->pkgpart,
851 feepart => $self->feepart,
852 # maybe should show quantity/unit discount?
854 foreach my $pkg_discount (@pkg_discounts) {
855 push @ext, $pkg_discount->description;
856 my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
857 $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
859 $d->{setup_amount} *= $self->quantity || 1; # ??
860 $d->{recur_amount} *= $self->quantity || 1; # ??
865 =item set_display OPTION => VALUE ...
867 A helper method for I<insert>, populates the pseudo-field B<display> with
868 appropriate FS::cust_bill_pkg_display objects.
870 Options are passed as a list of name/value pairs. Options are:
872 part_pkg: FS::part_pkg object from this line item's package.
874 real_pkgpart: if this line item comes from a bundled package, the pkgpart
875 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
880 my( $self, %opt ) = @_;
881 my $part_pkg = $opt{'part_pkg'};
882 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
884 my $conf = new FS::Conf;
886 # whether to break this down into setup/recur/usage
887 my $separate = $conf->exists('separate_usage');
889 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
890 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
892 # or use the category from $opt{'part_pkg'} if its not bundled?
893 my $categoryname = $cust_pkg->part_pkg->categoryname;
895 # if we don't have to separate setup/recur/usage, or put this in a
896 # package-specific section, or display a usage summary, then don't
897 # even create one of these. The item will just display in the unnamed
898 # section as a single line plus details.
899 return $self->set('display', [])
900 unless $separate || $categoryname || $usage_mandate;
904 my %hash = ( 'section' => $categoryname );
906 # whether to put usage details in a separate section, and if so, which one
907 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
908 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
910 # whether to show a usage summary line (total usage charges, no details)
911 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
912 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
915 # create lines for setup and (non-usage) recur, in the main section
916 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
917 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
919 # display everything in a single line
920 push @display, new FS::cust_bill_pkg_display
923 # and if usage_mandate is enabled, hide details
924 # (this only works on multisection invoices...)
925 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
929 if ($separate && $usage_section && $summary) {
930 # create a line for the usage summary in the main section
931 push @display, new FS::cust_bill_pkg_display { type => 'U',
937 if ($usage_mandate || ($usage_section && $summary) ) {
938 $hash{post_total} = 'Y';
941 if ($separate || $usage_mandate) {
942 # show call details for this line item in the usage section.
943 # if usage_mandate is on, this will display below the section subtotal.
944 # this also happens if usage is in a separate section and there's a
945 # summary in the main section, though I'm not sure why.
946 $hash{section} = $usage_section if $usage_section;
947 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
950 $self->set('display', \@display);
956 Returns a hash: keys are "setup", "recur" or usage classnum, values are
957 FS::cust_bill_pkg objects, each with no more than a single class (setup or
964 # XXX this goes away with cust_bill_pkg refactor
965 # or at least I wish it would, but it turns out to be harder than
968 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
969 my %cust_bill_pkg = ();
972 foreach my $classnum ($self->usage_classes) {
973 my $amount = $self->usage($classnum);
974 next if $amount == 0; # though if so we shouldn't be here
975 my $usage_item = FS::cust_bill_pkg->new({
979 'taxclass' => $classnum,
982 $cust_bill_pkg{$classnum} = $usage_item;
983 $usage_total += $amount;
986 foreach (qw(setup recur)) {
987 next if ($self->get($_) == 0);
988 my $item = FS::cust_bill_pkg->new({
995 $item->set($_, $self->get($_));
996 $cust_bill_pkg{$_} = $item;
1000 $cust_bill_pkg{recur}->set('recur',
1001 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1008 =item usage CLASSNUM
1010 Returns the amount of the charge associated with usage class CLASSNUM if
1011 CLASSNUM is defined. Otherwise returns the total charge associated with
1017 my( $self, $classnum ) = @_;
1018 $self->regularize_details;
1020 if ( $self->get('details') ) {
1023 map { $_->amount || 0 }
1024 grep { !defined($classnum) or $classnum eq $_->classnum }
1025 @{ $self->get('details') }
1030 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1031 ' WHERE billpkgnum = '. $self->billpkgnum;
1032 if (defined $classnum) {
1033 if ($classnum =~ /^(\d+)$/) {
1034 $sql .= " AND classnum = $1";
1035 } elsif ($classnum eq '') {
1036 $sql .= " AND classnum IS NULL";
1040 my $sth = dbh->prepare($sql) or die dbh->errstr;
1041 $sth->execute or die $sth->errstr;
1043 return $sth->fetchrow_arrayref->[0] || 0;
1051 Returns a list of usage classnums associated with this invoice line's
1058 $self->regularize_details;
1060 if ( $self->get('details') ) {
1062 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1067 map { $_->classnum }
1068 qsearch({ table => 'cust_bill_pkg_detail',
1069 hashref => { billpkgnum => $self->billpkgnum },
1070 select => 'DISTINCT classnum',
1077 sub cust_tax_exempt_pkg {
1080 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1083 =item cust_bill_pkg_tax_Xlocation
1085 Returns the list of associated cust_bill_pkg_tax_location and/or
1086 cust_bill_pkg_tax_rate_location objects
1090 sub cust_bill_pkg_tax_Xlocation {
1093 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1096 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1097 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1102 =item recur_show_zero
1104 Whether to show a zero recurring amount. This is true if the package or its
1105 definition has the recur_show_zero flag, and the recurring fee is actually
1106 zero for this period.
1110 sub recur_show_zero {
1111 my( $self, $what ) = @_;
1113 return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1115 $self->cust_pkg->_X_show_zero('recur');
1118 =item setup_show_zero
1120 Whether to show a zero setup charge. This requires the package or its
1121 definition to have the setup_show_zero flag, but it also returns false if
1122 the package's setup date is before this line item's start date.
1126 sub setup_show_zero {
1128 return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1129 my $cust_pkg = $self->cust_pkg;
1130 return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1131 return $cust_pkg->_X_show_zero('setup');
1134 =item credited [ BEFORE, AFTER, OPTIONS ]
1136 Returns the sum of credits applied to this item. Arguments are the same as
1137 owed_sql/paid_sql/credited_sql.
1143 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1146 =item tax_locationnum
1148 Returns the L<FS::cust_location> number that this line item is in for tax
1149 purposes. For package sales, it's the package tax location; for fees,
1150 it's the customer's default service location.
1154 sub tax_locationnum {
1156 if ( $self->pkgnum ) { # normal sales
1157 return $self->cust_pkg->tax_locationnum;
1158 } elsif ( $self->feepart ) { # fees
1159 my $custnum = $self->fee_origin->custnum;
1161 return FS::cust_main->by_key($custnum)->ship_locationnum;
1170 if ( $self->pkgnum ) { # normal sales
1171 return $self->cust_pkg->tax_location;
1172 } elsif ( $self->feepart ) { # fees
1173 my $custnum = $self->fee_origin->custnum;
1175 return FS::cust_main->by_key($custnum)->ship_location;
1184 =head1 CLASS METHODS
1190 Returns an SQL expression for the total usage charges in details on
1196 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1197 FROM cust_bill_pkg_detail
1198 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1200 sub usage_sql { $usage_sql }
1202 # this makes owed_sql, etc. much more concise
1204 my ($class, $start, $end, %opt) = @_;
1205 my $setuprecur = $opt{setuprecur} || '';
1207 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1208 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1209 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1211 if ($opt{no_usage} and $charged =~ /recur/) {
1212 $charged = "$charged - $usage_sql"
1219 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1221 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1222 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1223 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1229 '(' . $class->charged_sql(@_) .
1230 ' - ' . $class->paid_sql(@_) .
1231 ' - ' . $class->credited_sql(@_) . ')'
1234 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1236 Returns an SQL expression for the sum of payments applied to this item.
1241 my ($class, $start, $end, %opt) = @_;
1242 my $s = $start ? "AND cust_pay._date <= $start" : '';
1243 my $e = $end ? "AND cust_pay._date > $end" : '';
1244 my $setuprecur = $opt{setuprecur} || '';
1245 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1246 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1247 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1249 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1250 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1251 JOIN cust_pay USING (paynum)
1252 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1253 $s $e $setuprecur )";
1255 if ( $opt{no_usage} ) {
1256 # cap the amount paid at the sum of non-usage charges,
1257 # minus the amount credited against non-usage charges
1259 $class->charged_sql($start, $end, %opt) . ' - ' .
1260 $class->credited_sql($start, $end, %opt).')';
1269 my ($class, $start, $end, %opt) = @_;
1270 my $s = $start ? "AND cust_credit._date <= $start" : '';
1271 my $e = $end ? "AND cust_credit._date > $end" : '';
1272 my $setuprecur = $opt{setuprecur} || '';
1273 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1274 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1275 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1277 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1278 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1279 JOIN cust_credit USING (crednum)
1280 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1281 $s $e $setuprecur )";
1283 if ( $opt{no_usage} ) {
1284 # cap the amount credited at the sum of non-usage charges
1285 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1293 sub upgrade_tax_location {
1294 # For taxes that were calculated/invoiced before cust_location refactoring
1295 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1296 # they were calculated on a package-location basis. Create them here,
1297 # along with any necessary cust_location records and any tax exemption
1300 my ($class, %opt) = @_;
1301 # %opt may include 's' and 'e': start and end date ranges
1302 # and 'X': abort on any error, instead of just rolling back changes to
1305 my $oldAutoCommit = $FS::UID::AutoCommit;
1306 local $FS::UID::AutoCommit = 0;
1309 use FS::h_cust_main;
1310 use FS::h_cust_bill;
1312 use FS::h_cust_main_exemption;
1315 local $FS::cust_location::import = 1;
1317 my $conf = FS::Conf->new; # h_conf?
1318 return if $conf->config('tax_data_vendor'); #don't touch this case
1319 my $use_ship = $conf->exists('tax-ship_address');
1320 my $use_pkgloc = $conf->exists('tax-pkg_address');
1322 my $date_where = '';
1324 $date_where .= " AND cust_bill._date >= $opt{s}";
1327 $date_where .= " AND cust_bill._date < $opt{e}";
1330 my $commit_each_invoice = 1 unless $opt{X};
1332 # if an invoice has either of these kinds of objects, then it doesn't
1333 # need to be upgraded...probably
1334 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1335 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1336 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1337 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1338 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1339 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1340 ' AND exempt_monthly IS NULL';
1342 my %all_tax_names = (
1345 map { $_->taxname => 1 }
1346 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1349 my $search = FS::Cursor->new({
1350 table => 'cust_bill',
1352 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1353 "AND NOT EXISTS($sub_has_exempt) ".
1357 #print "Processing ".scalar(@invnums)." invoices...\n";
1361 while (my $cust_bill = $search->fetch) {
1362 my $invnum = $cust_bill->invnum;
1364 print STDERR "Invoice #$invnum\n";
1366 my %pkgpart_taxclass; # pkgpart => taxclass
1367 my %pkgpart_exempt_setup;
1368 my %pkgpart_exempt_recur;
1369 my $h_cust_bill = qsearchs('h_cust_bill',
1370 { invnum => $invnum,
1371 history_action => 'insert' });
1372 if (!$h_cust_bill) {
1373 warn "no insert record for invoice $invnum; skipped\n";
1374 #$date = $cust_bill->_date as a fallback?
1375 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1376 # when looking up history records in other tables.
1379 my $custnum = $h_cust_bill->custnum;
1381 # Determine the address corresponding to this tax region.
1382 # It's either the bill or ship address of the customer as of the
1383 # invoice date-of-insertion. (Not necessarily the invoice date.)
1384 my $date = $h_cust_bill->history_date;
1385 local($FS::Record::qsearch_qualify_columns) = 0;
1386 my $h_cust_main = qsearchs('h_cust_main',
1387 { custnum => $custnum },
1388 FS::h_cust_main->sql_h_searchs($date)
1390 if (!$h_cust_main ) {
1391 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1393 # fallback to current $cust_main? sounds dangerous.
1396 # This is a historical customer record, so it has a historical address.
1397 # If there's no cust_location matching this custnum and address (there
1398 # probably isn't), create one.
1399 my %tax_loc; # keys are pkgnums, values are cust_location objects
1400 my $default_tax_loc;
1401 if ( $h_cust_main->bill_locationnum ) {
1402 # the location has already been upgraded
1404 $default_tax_loc = $h_cust_main->ship_location;
1406 $default_tax_loc = $h_cust_main->bill_location;
1409 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1410 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1411 FS::cust_main->location_fields;
1412 # not really needed for this, and often result in duplicate locations
1413 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1415 $hash{custnum} = $h_cust_main->custnum;
1416 $default_tax_loc = FS::cust_location->new(\%hash);
1417 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1419 warn "couldn't create historical location record for cust#".
1420 $h_cust_main->custnum.": $error\n";
1425 $exempt_cust = 1 if $h_cust_main->tax;
1427 # classify line items
1429 my %nontax_items; # taxclass => array of cust_bill_pkg
1430 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1431 my $pkgnum = $item->pkgnum;
1433 if ( $pkgnum == 0 ) {
1435 push @tax_items, $item;
1438 # (pkgparts really shouldn't change, right?)
1439 local($FS::Record::qsearch_qualify_columns) = 0;
1440 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1441 FS::h_cust_pkg->sql_h_searchs($date)
1443 if ( !$h_cust_pkg ) {
1444 warn "no historical package #".$item->pkgpart."; skipped\n";
1447 my $pkgpart = $h_cust_pkg->pkgpart;
1449 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1450 # then this package already had a locationnum assigned, and that's
1451 # the one to use for tax calculation
1452 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1454 # use the customer's bill or ship loc, which was inserted earlier
1455 $tax_loc{$pkgnum} = $default_tax_loc;
1458 if (!exists $pkgpart_taxclass{$pkgpart}) {
1459 local($FS::Record::qsearch_qualify_columns) = 0;
1460 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1461 FS::h_part_pkg->sql_h_searchs($date)
1463 if ( !$h_part_pkg ) {
1464 warn "no historical package def #$pkgpart; skipped\n";
1467 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1468 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1469 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1472 # mark any exemptions that apply
1473 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1474 $item->set('exempt_setup' => 1);
1477 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1478 $item->set('exempt_recur' => 1);
1481 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1483 $nontax_items{$taxclass} ||= [];
1484 push @{ $nontax_items{$taxclass} }, $item;
1488 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1491 # Get any per-customer taxname exemptions that were in effect.
1492 my %exempt_cust_taxname;
1493 foreach (keys %all_tax_names) {
1494 local($FS::Record::qsearch_qualify_columns) = 0;
1495 my $h_exemption = qsearchs('h_cust_main_exemption', {
1496 'custnum' => $custnum,
1499 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1502 $exempt_cust_taxname{ $_ } = 1;
1506 # Use a variation on the procedure in
1507 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1509 my @loc_keys = qw( district city county state country );
1510 my %taxdef_by_name; # by name, and then by taxclass
1511 my %est_tax; # by name, and then by taxclass
1512 my %taxable_items; # by taxnum, and then an array
1514 foreach my $taxclass (keys %nontax_items) {
1515 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1516 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1517 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1518 my @elim = qw( district city county state );
1519 my @taxdefs; # because there may be several with different taxnames
1521 $myhash{taxclass} = $taxclass;
1522 @taxdefs = qsearch('cust_main_county', \%myhash);
1524 $myhash{taxclass} = '';
1525 @taxdefs = qsearch('cust_main_county', \%myhash);
1527 $myhash{ shift @elim } = '';
1528 } while scalar(@elim) and !@taxdefs;
1530 foreach my $taxdef (@taxdefs) {
1531 next if $taxdef->tax == 0;
1532 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1534 $taxable_items{$taxdef->taxnum} ||= [];
1535 # clone the item so that taxdef-dependent changes don't
1536 # change it for other taxdefs
1537 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1539 # these flags are already set if the part_pkg declares itself exempt
1540 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1541 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1544 my $taxable = $item->setup + $item->recur;
1546 # h_cust_credit_bill_pkg?
1547 # NO. Because if these exemptions HAD been created at the time of
1548 # billing, and then a credit applied later, the exemption would
1549 # have been adjusted by the amount of the credit. So we adjust
1550 # the taxable amount before creating the exemption.
1551 # But don't deduct the credit from taxable, because the tax was
1552 # calculated before the credit was applied.
1553 foreach my $f (qw(setup recur)) {
1554 my $credited = FS::Record->scalar_sql(
1555 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1556 "WHERE billpkgnum = ? AND setuprecur = ?",
1560 $item->set($f, $item->get($f) - $credited) if $credited;
1562 my $existing_exempt = FS::Record->scalar_sql(
1563 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1564 "billpkgnum = ? AND taxnum = ?",
1565 $item->billpkgnum, $taxdef->taxnum
1567 $taxable -= $existing_exempt;
1569 if ( $taxable and $exempt_cust ) {
1570 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1573 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1574 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1577 if ( $taxable and $item->exempt_setup ) {
1578 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1579 $taxable -= $item->setup;
1581 if ( $taxable and $item->exempt_recur ) {
1582 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1583 $taxable -= $item->recur;
1586 $item->set('taxable' => $taxable);
1587 push @{ $taxable_items{$taxdef->taxnum} }, $item
1590 # estimate the amount of tax (this is necessary because different
1591 # taxdefs with the same taxname may have different tax rates)
1592 # and sum that for each taxname/taxclass combination
1594 $est_tax{$taxdef->taxname} ||= {};
1595 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1596 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1597 $taxable * $taxdef->tax;
1599 foreach (@new_exempt) {
1600 next if $_->{amount} == 0;
1601 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1603 billpkgnum => $item->billpkgnum,
1604 taxnum => $taxdef->taxnum,
1606 my $error = $cust_tax_exempt_pkg->insert;
1608 my $pkgnum = $item->pkgnum;
1609 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1613 } #foreach @new_exempt
1616 } #foreach $taxclass
1618 # Now go through the billed taxes and match them up with the line items.
1619 TAX_ITEM: foreach my $tax_item ( @tax_items )
1621 my $taxname = $tax_item->itemdesc;
1622 $taxname = '' if $taxname eq 'Tax';
1624 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1625 # then we didn't find any applicable taxes with this name
1626 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1627 # possibly all of these should be "next TAX_ITEM", but whole invoices
1628 # are transaction protected and we can go back and retry them.
1631 # classname => cust_main_county
1632 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1634 # Divide the tax item among taxclasses, if necessary
1635 # classname => estimated tax amount
1636 my $this_est_tax = $est_tax{$taxname};
1637 if (!defined $this_est_tax) {
1638 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1641 my $est_total = sum(values %$this_est_tax);
1642 if ( $est_total == 0 ) {
1644 warn "estimated tax on invoice #$invnum is zero.\n";
1648 my $real_tax = $tax_item->setup;
1649 printf ("Distributing \$%.2f tax:\n", $real_tax);
1650 my $cents_remaining = $real_tax * 100; # for rounding error
1651 my @tax_links; # partial CBPTL hashrefs
1652 foreach my $taxclass (keys %taxdef_by_class) {
1653 my $taxdef = $taxdef_by_class{$taxclass};
1654 # these items already have "taxable" set to their charge amount
1655 # after applying any credits or exemptions
1656 my @items = @{ $taxable_items{$taxdef->taxnum} };
1657 my $subtotal = sum(map {$_->get('taxable')} @items);
1658 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1660 foreach my $nontax (@items) {
1661 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1662 my $part = int($real_tax
1664 * ($this_est_tax->{$taxclass}/$est_total)
1666 * ($nontax->get('taxable'))/$subtotal
1670 $cents_remaining -= $part;
1672 taxnum => $taxdef->taxnum,
1673 pkgnum => $nontax->pkgnum,
1674 locationnum => $my_tax_loc->locationnum,
1675 billpkgnum => $nontax->billpkgnum,
1679 } #foreach $taxclass
1680 # Distribute any leftover tax round-robin style, one cent at a time.
1682 my $nlinks = scalar(@tax_links);
1684 # ensure that it really is an integer
1685 $cents_remaining = sprintf('%.0f', $cents_remaining);
1686 while ($cents_remaining > 0) {
1687 $tax_links[$i % $nlinks]->{cents} += 1;
1692 warn "Can't create tax links--no taxable items found.\n";
1696 # Gather credit/payment applications so that we can link them
1699 qsearch( 'cust_credit_bill_pkg',
1700 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1702 qsearch( 'cust_bill_pay_pkg',
1703 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1707 # grab the first one
1708 my $this_unlinked = shift @unlinked;
1709 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1711 # Create tax links (yay!)
1712 printf("Creating %d tax links.\n",scalar(@tax_links));
1713 foreach (@tax_links) {
1714 my $link = FS::cust_bill_pkg_tax_location->new({
1715 billpkgnum => $tax_item->billpkgnum,
1716 taxtype => 'FS::cust_main_county',
1717 locationnum => $_->{locationnum},
1718 taxnum => $_->{taxnum},
1719 pkgnum => $_->{pkgnum},
1720 amount => sprintf('%.2f', $_->{cents} / 100),
1721 taxable_billpkgnum => $_->{billpkgnum},
1723 my $error = $link->insert;
1725 warn "Can't create tax link for inv#$invnum: $error\n";
1729 my $link_cents = $_->{cents};
1730 # update/create subitem links
1732 # If $this_unlinked is undef, then we've allocated all of the
1733 # credit/payment applications to the tax item. If $link_cents is 0,
1734 # then we've applied credits/payments to all of this package fraction,
1735 # so go on to the next.
1736 while ($this_unlinked and $link_cents) {
1737 # apply as much as possible of $link_amount to this credit/payment
1739 my $apply_cents = min($link_cents, $unlinked_cents);
1740 $link_cents -= $apply_cents;
1741 $unlinked_cents -= $apply_cents;
1742 # $link_cents or $unlinked_cents or both are now zero
1743 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1744 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1745 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1746 if ( $this_unlinked->$pkey ) {
1747 # then it's an existing link--replace it
1748 $error = $this_unlinked->replace;
1750 $this_unlinked->insert;
1752 # what do we do with errors at this stage?
1754 warn "Error creating tax application link: $error\n";
1755 next INVOICE; # for lack of a better idea
1758 if ( $unlinked_cents == 0 ) {
1759 # then we've allocated all of this payment/credit application,
1760 # so grab the next one
1761 $this_unlinked = shift @unlinked;
1762 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1763 } elsif ( $link_cents == 0 ) {
1764 # then we've covered all of this package tax fraction, so split
1765 # off a new application from this one
1766 $this_unlinked = $this_unlinked->new({
1767 $this_unlinked->hash,
1770 # $unlinked_cents is still what it is
1773 } #while $this_unlinked and $link_cents
1774 } #foreach (@tax_links)
1775 } #foreach $tax_item
1777 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1783 $dbh->rollback if $oldAutoCommit;
1784 die "Upgrade halted.\n" unless $commit_each_invoice;
1788 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1793 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1797 use Date::Parse 'str2time';
1800 my $upgrade = 'tax_location_2012';
1801 return if FS::upgrade_journal->is_done($upgrade);
1802 my $job = FS::queue->new({
1803 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1805 # call it kind of like a class method, not that it matters much
1806 $job->insert($class, 's' => str2time('2012-01-01'));
1807 # if there's a customer location upgrade queued also, wait for it to
1809 my $location_job = qsearchs('queue', {
1810 job => 'FS::cust_main::Location::process_upgrade_location'
1812 if ( $location_job ) {
1813 $job->depend_insert($location_job->jobnum);
1815 # Then mark the upgrade as done, so that we don't queue the job twice
1816 # and somehow run two of them concurrently.
1817 FS::upgrade_journal->set_done($upgrade);
1818 # This upgrade now does the job of assigning taxable_billpkgnums to
1819 # cust_bill_pkg_tax_location, so set that task done also.
1820 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1827 setup and recur shouldn't be separate fields. There should be one "amount"
1828 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1830 A line item with both should really be two separate records (preserving
1831 sdate and edate for setup fees for recurring packages - that information may
1832 be valuable later). Invoice generation (cust_main::bill), invoice printing
1833 (cust_bill), tax reports (report_tax.cgi) and line item reports
1834 (cust_bill_pkg.cgi) would need to be updated.
1836 owed_setup and owed_recur could then be repaced by just owed, and
1837 cust_bill::open_cust_bill_pkg and
1838 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1840 The upgrade procedure is pretty sketchy.
1844 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1845 from the base documentation.