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 [ , REPROCESS_CDRS ] ]
329 Voids this line item: deletes the line item and adds a record of the voided
330 line item to the FS::cust_bill_pkg_void table (and related tables).
336 my $reason = scalar(@_) ? shift : '';
337 my $reprocess_cdrs = scalar(@_) ? shift : '';
339 unless (ref($reason) || !$reason) {
340 $reason = FS::reason->new_or_existing(
342 'type' => 'Invoice void',
347 local $SIG{HUP} = 'IGNORE';
348 local $SIG{INT} = 'IGNORE';
349 local $SIG{QUIT} = 'IGNORE';
350 local $SIG{TERM} = 'IGNORE';
351 local $SIG{TSTP} = 'IGNORE';
352 local $SIG{PIPE} = 'IGNORE';
354 my $oldAutoCommit = $FS::UID::AutoCommit;
355 local $FS::UID::AutoCommit = 0;
358 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
359 map { $_ => $self->get($_) } $self->fields
361 $cust_bill_pkg_void->reasonnum($reason->reasonnum) if $reason;
362 my $error = $cust_bill_pkg_void->insert;
364 $dbh->rollback if $oldAutoCommit;
368 foreach my $table (qw(
370 cust_bill_pkg_display
371 cust_bill_pkg_discount
372 cust_bill_pkg_tax_location
373 cust_bill_pkg_tax_rate_location
377 my %delete_args = ();
378 $delete_args{'reprocess_cdrs'} = $reprocess_cdrs
379 if $table eq 'cust_bill_pkg_detail';
381 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
383 my $vclass = 'FS::'.$table.'_void';
384 my $void = $vclass->new( {
385 map { $_ => $linked->get($_) } $linked->fields
387 my $error = $void->insert || $linked->delete(%delete_args);
389 $dbh->rollback if $oldAutoCommit;
397 $error = $self->delete;
399 $dbh->rollback if $oldAutoCommit;
403 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
418 local $SIG{HUP} = 'IGNORE';
419 local $SIG{INT} = 'IGNORE';
420 local $SIG{QUIT} = 'IGNORE';
421 local $SIG{TERM} = 'IGNORE';
422 local $SIG{TSTP} = 'IGNORE';
423 local $SIG{PIPE} = 'IGNORE';
425 my $oldAutoCommit = $FS::UID::AutoCommit;
426 local $FS::UID::AutoCommit = 0;
429 foreach my $table (qw(
431 cust_bill_pkg_display
432 cust_bill_pkg_discount
433 cust_bill_pkg_tax_location
434 cust_bill_pkg_tax_rate_location
441 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
442 my $error = $linked->delete;
444 $dbh->rollback if $oldAutoCommit;
451 foreach my $cust_tax_adjustment (
452 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
454 $cust_tax_adjustment->billpkgnum(''); #NULL
455 my $error = $cust_tax_adjustment->replace;
457 $dbh->rollback if $oldAutoCommit;
462 my $error = $self->SUPER::delete(@_);
464 $dbh->rollback if $oldAutoCommit;
468 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 #alas, bin/follow-tax-rename
476 #=item replace OLD_RECORD
478 #Currently unimplemented. This would be even more of an accounting nightmare
479 #than deleteing the items. Just don't do it.
484 # return "Can't modify cust_bill_pkg records!";
489 Checks all fields to make sure this is a valid line item. If there is an
490 error, returns the error, otherwise returns false. Called by the insert
499 $self->ut_numbern('billpkgnum')
500 || $self->ut_snumber('pkgnum')
501 || $self->ut_number('invnum')
502 || $self->ut_money('setup')
503 || $self->ut_moneyn('unitsetup')
504 || $self->ut_currencyn('setup_billed_currency')
505 || $self->ut_moneyn('setup_billed_amount')
506 || $self->ut_money('recur')
507 || $self->ut_moneyn('unitrecur')
508 || $self->ut_currencyn('recur_billed_currency')
509 || $self->ut_moneyn('recur_billed_amount')
510 || $self->ut_numbern('sdate')
511 || $self->ut_numbern('edate')
512 || $self->ut_textn('itemdesc')
513 || $self->ut_textn('itemcomment')
514 || $self->ut_enum('hidden', [ '', 'Y' ])
516 return $error if $error;
518 $self->regularize_details;
520 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
521 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
522 return "Unknown pkgnum ". $self->pkgnum
523 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
526 return "Unknown invnum"
527 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
532 =item regularize_details
534 Converts the contents of the 'details' pseudo-field to
535 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
539 sub regularize_details {
541 if ( $self->get('details') ) {
542 foreach my $detail ( @{$self->get('details')} ) {
543 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
544 # then turn it into one
546 if ( ! ref($detail) ) {
547 $hash{'detail'} = $detail;
549 elsif ( ref($detail) eq 'HASH' ) {
552 elsif ( ref($detail) eq 'ARRAY' ) {
553 carp "passing invoice details as arrays is deprecated";
554 #carp "this way sucks, use a hash"; #but more useful/friendly
555 $hash{'format'} = $detail->[0];
556 $hash{'detail'} = $detail->[1];
557 $hash{'amount'} = $detail->[2];
558 $hash{'classnum'} = $detail->[3];
559 $hash{'phonenum'} = $detail->[4];
560 $hash{'accountcode'} = $detail->[5];
561 $hash{'startdate'} = $detail->[6];
562 $hash{'duration'} = $detail->[7];
563 $hash{'regionname'} = $detail->[8];
566 die "unknown detail type ". ref($detail);
568 $detail = new FS::cust_bill_pkg_detail \%hash;
570 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
576 =item set_exemptions TAXOBJECT, OPTIONS
578 Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
579 L<FS::tax_rate> record for the tax.
581 This will deal with the following cases:
585 =item Fully exempt customers (cust_main.tax flag) or customer classes
588 =item Customers exempt from specific named taxes (cust_main_exemption
591 =item Taxes that don't apply to setup or recurring fees
592 (cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
594 =item Packages that are marked as tax-exempt (part_pkg.setuptax,
597 =item Fees that aren't marked as taxable (part_fee.taxable).
601 It does NOT deal with monthly tax exemptions, which need more context
602 than this humble little method cares to deal with.
604 OPTIONS should include "custnum" => the customer number if this tax line
605 hasn't been inserted (which it probably hasn't).
607 Returns a list of exemption objects, which will also be attached to the
608 line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
609 item will insert these records as well.
618 my $part_pkg = $self->part_pkg;
619 my $part_fee = $self->part_fee;
622 my $custnum = $opt{custnum};
623 $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
625 $cust_main = FS::cust_main->by_key( $custnum )
626 or die "set_exemptions can't identify customer (pass custnum option)\n";
629 my $taxable_charged = $self->setup + $self->recur;
630 return unless $taxable_charged > 0;
632 ### Fully exempt customer ###
634 my $conf = FS::Conf->new;
635 if ( $conf->exists('cust_class-tax_exempt') ) {
636 my $cust_class = $cust_main->cust_class;
637 $exempt_cust = $cust_class->tax if $cust_class;
639 $exempt_cust = $cust_main->tax;
642 ### Exemption from named tax ###
643 my $exempt_cust_taxname;
644 if ( !$exempt_cust and $tax->taxname ) {
645 $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
648 if ( $exempt_cust ) {
650 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
651 amount => $taxable_charged,
654 $taxable_charged = 0;
656 } elsif ( $exempt_cust_taxname ) {
658 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
659 amount => $taxable_charged,
660 exempt_cust_taxname => 'Y',
662 $taxable_charged = 0;
666 my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
667 or ($part_pkg and $part_pkg->setuptax)
672 and $taxable_charged > 0 ) {
674 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
675 amount => $self->setup,
678 $taxable_charged -= $self->setup;
682 my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
683 or ($part_pkg and $part_pkg->recurtax)
688 and $taxable_charged > 0 ) {
690 push @new_exemptions, FS::cust_tax_exempt_pkg->new({
691 amount => $self->recur,
694 $taxable_charged -= $self->recur;
698 foreach (@new_exemptions) {
699 $_->set('taxnum', $tax->taxnum);
700 $_->set('taxtype', ref($tax));
703 push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
704 return @new_exemptions;
710 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
714 Returns the customer (L<FS::cust_main> object) for this line item.
719 # required for cust_main_Mixin equivalence
720 # and use cust_bill instead of cust_pkg because this might not have a
723 my $cust_bill = $self->cust_bill or return '';
724 $cust_bill->cust_main;
727 =item previous_cust_bill_pkg
729 Returns the previous cust_bill_pkg for this package, if any.
733 sub previous_cust_bill_pkg {
735 return unless $self->sdate;
737 'table' => 'cust_bill_pkg',
738 'hashref' => { 'pkgnum' => $self->pkgnum,
739 'sdate' => { op=>'<', value=>$self->sdate },
741 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
747 Returns the amount owed (still outstanding) on this line item's setup 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('setup', @_);
761 Returns the amount owed (still outstanding) on this line item's recurring fee,
762 which is the amount of the line item minus all payment applications (see
763 L<FS::cust_bill_pay_pkg> and credit applications (see
764 L<FS::cust_credit_bill_pkg>).
770 $self->owed('recur', @_);
773 # modeled after cust_bill::owed...
775 my( $self, $field ) = @_;
776 my $balance = $self->$field();
777 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
778 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
779 $balance = sprintf( '%.2f', $balance );
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
786 my( $self, $field ) = @_;
787 my $balance = $self->$field();
788 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
789 $balance = sprintf( '%.2f', $balance );
790 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
794 sub cust_bill_pay_pkg {
795 my( $self, $field ) = @_;
796 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
797 'setuprecur' => $field,
802 sub cust_credit_bill_pkg {
803 my( $self, $field ) = @_;
804 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
805 'setuprecur' => $field,
812 Returns the number of billing units (for tax purposes) represented by this,
819 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
824 If this item has any discounts, returns a hashref in the format used
825 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
826 on an invoice. This will contain the keys 'description', 'amount',
827 'ext_description' (an arrayref of text lines describing the discounts),
828 and '_is_discount' (a flag).
830 The value for 'amount' will be negative, and will be scaled for the package
839 my $d; # this will be returned.
841 my @pkg_discounts = $self->pkg_discount;
842 if (@pkg_discounts) {
843 # special case: if there are old "discount details" on this line item,
844 # don't show discount line items
845 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
852 description => $self->mt('Discount'),
855 ext_description => \@ext,
856 pkgpart => $self->pkgpart,
857 feepart => $self->feepart,
858 # maybe should show quantity/unit discount?
860 foreach my $pkg_discount (@pkg_discounts) {
861 push @ext, $pkg_discount->description;
862 my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
863 $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
867 # show introductory rate as a pseudo-discount
868 if (!$d) { # this will conflict with showing real discounts
869 my $part_pkg = $self->part_pkg;
870 if ( $part_pkg and $part_pkg->option('show_as_discount',1) ) {
871 my $cust_pkg = $self->cust_pkg;
872 my $intro_end = $part_pkg->intro_end($cust_pkg);
873 my $_date = $self->cust_bill->_date;
874 if ( $intro_end > $_date ) {
875 $d = $part_pkg->item_discount($cust_pkg);
881 $d->{setup_amount} *= $self->quantity || 1; # ??
882 $d->{recur_amount} *= $self->quantity || 1; # ??
888 =item set_display OPTION => VALUE ...
890 A helper method for I<insert>, populates the pseudo-field B<display> with
891 appropriate FS::cust_bill_pkg_display objects.
893 Options are passed as a list of name/value pairs. Options are:
895 part_pkg: FS::part_pkg object from this line item's package.
897 real_pkgpart: if this line item comes from a bundled package, the pkgpart
898 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
903 my( $self, %opt ) = @_;
904 my $part_pkg = $opt{'part_pkg'};
905 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
907 my $conf = new FS::Conf;
909 # whether to break this down into setup/recur/usage
910 my $separate = $conf->exists('separate_usage');
912 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
913 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
915 # or use the category from $opt{'part_pkg'} if its not bundled?
916 my $categoryname = $cust_pkg->part_pkg->categoryname;
918 # if we don't have to separate setup/recur/usage, or put this in a
919 # package-specific section, or display a usage summary, then don't
920 # even create one of these. The item will just display in the unnamed
921 # section as a single line plus details.
922 return $self->set('display', [])
923 unless $separate || $categoryname || $usage_mandate;
927 my %hash = ( 'section' => $categoryname );
929 # whether to put usage details in a separate section, and if so, which one
930 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
931 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
933 # whether to show a usage summary line (total usage charges, no details)
934 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
935 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
938 # create lines for setup and (non-usage) recur, in the main section
939 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
940 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
942 # display everything in a single line
943 push @display, new FS::cust_bill_pkg_display
946 # and if usage_mandate is enabled, hide details
947 # (this only works on multisection invoices...)
948 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
952 if ($separate && $usage_section && $summary) {
953 # create a line for the usage summary in the main section
954 push @display, new FS::cust_bill_pkg_display { type => 'U',
960 if ($usage_mandate || ($usage_section && $summary) ) {
961 $hash{post_total} = 'Y';
964 if ($separate || $usage_mandate) {
965 # show call details for this line item in the usage section.
966 # if usage_mandate is on, this will display below the section subtotal.
967 # this also happens if usage is in a separate section and there's a
968 # summary in the main section, though I'm not sure why.
969 $hash{section} = $usage_section if $usage_section;
970 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
973 $self->set('display', \@display);
979 Returns a hash: keys are "setup", "recur" or usage classnum, values are
980 FS::cust_bill_pkg objects, each with no more than a single class (setup or
987 # XXX this goes away with cust_bill_pkg refactor
988 # or at least I wish it would, but it turns out to be harder than
991 #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
992 my %cust_bill_pkg = ();
995 foreach my $classnum ($self->usage_classes) {
996 my $amount = $self->usage($classnum);
997 next if $amount == 0; # though if so we shouldn't be here
998 my $usage_item = FS::cust_bill_pkg->new({
1002 'taxclass' => $classnum,
1005 $cust_bill_pkg{$classnum} = $usage_item;
1006 $usage_total += $amount;
1009 foreach (qw(setup recur)) {
1010 next if ($self->get($_) == 0);
1011 my $item = FS::cust_bill_pkg->new({
1018 $item->set($_, $self->get($_));
1019 $cust_bill_pkg{$_} = $item;
1023 $cust_bill_pkg{recur}->set('recur',
1024 sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
1031 =item usage CLASSNUM
1033 Returns the amount of the charge associated with usage class CLASSNUM if
1034 CLASSNUM is defined. Otherwise returns the total charge associated with
1040 my( $self, $classnum ) = @_;
1041 $self->regularize_details;
1043 if ( $self->get('details') ) {
1046 map { $_->amount || 0 }
1047 grep { !defined($classnum) or $classnum eq $_->classnum }
1048 @{ $self->get('details') }
1053 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
1054 ' WHERE billpkgnum = '. $self->billpkgnum;
1055 if (defined $classnum) {
1056 if ($classnum =~ /^(\d+)$/) {
1057 $sql .= " AND classnum = $1";
1058 } elsif ($classnum eq '') {
1059 $sql .= " AND classnum IS NULL";
1063 my $sth = dbh->prepare($sql) or die dbh->errstr;
1064 $sth->execute or die $sth->errstr;
1066 return $sth->fetchrow_arrayref->[0] || 0;
1074 Returns a list of usage classnums associated with this invoice line's
1081 $self->regularize_details;
1083 if ( $self->get('details') ) {
1085 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
1090 map { $_->classnum }
1091 qsearch({ table => 'cust_bill_pkg_detail',
1092 hashref => { billpkgnum => $self->billpkgnum },
1093 select => 'DISTINCT classnum',
1100 sub cust_tax_exempt_pkg {
1103 my $array = $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
1106 =item cust_bill_pkg_tax_Xlocation
1108 Returns the list of associated cust_bill_pkg_tax_location and/or
1109 cust_bill_pkg_tax_rate_location objects
1113 sub cust_bill_pkg_tax_Xlocation {
1116 my %hash = ( 'billpkgnum' => $self->billpkgnum );
1119 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
1120 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
1125 =item recur_show_zero
1127 Whether to show a zero recurring amount. This is true if the package or its
1128 definition has the recur_show_zero flag, and the recurring fee is actually
1129 zero for this period.
1133 sub recur_show_zero {
1134 my( $self, $what ) = @_;
1136 return 0 unless $self->get('recur') == 0 && $self->pkgnum;
1138 $self->cust_pkg->_X_show_zero('recur');
1141 =item setup_show_zero
1143 Whether to show a zero setup charge. This requires the package or its
1144 definition to have the setup_show_zero flag, but it also returns false if
1145 the package's setup date is before this line item's start date.
1149 sub setup_show_zero {
1151 return 0 unless $self->get('setup') == 0 && $self->pkgnum;
1152 my $cust_pkg = $self->cust_pkg;
1153 return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
1154 return $cust_pkg->_X_show_zero('setup');
1157 =item credited [ BEFORE, AFTER, OPTIONS ]
1159 Returns the sum of credits applied to this item. Arguments are the same as
1160 owed_sql/paid_sql/credited_sql.
1166 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1169 =item tax_locationnum
1171 Returns the L<FS::cust_location> number that this line item is in for tax
1172 purposes. For package sales, it's the package tax location; for fees,
1173 it's the customer's default service location.
1177 sub tax_locationnum {
1179 if ( $self->pkgnum ) { # normal sales
1180 return $self->cust_pkg->tax_locationnum;
1181 } elsif ( $self->feepart ) { # fees
1182 my $custnum = $self->fee_origin->custnum;
1184 return FS::cust_main->by_key($custnum)->ship_locationnum;
1193 if ( $self->pkgnum ) { # normal sales
1194 return $self->cust_pkg->tax_location;
1195 } elsif ( $self->feepart ) { # fees
1196 my $fee_origin = $self->fee_origin;
1197 if ( $fee_origin ) {
1198 my $custnum = $fee_origin->custnum;
1200 return FS::cust_main->by_key($custnum)->ship_location;
1210 =head1 CLASS METHODS
1216 Returns an SQL expression for the total usage charges in details on
1222 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1223 FROM cust_bill_pkg_detail
1224 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1226 sub usage_sql { $usage_sql }
1228 # this makes owed_sql, etc. much more concise
1230 my ($class, $start, $end, %opt) = @_;
1231 my $setuprecur = $opt{setuprecur} || '';
1233 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1234 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1235 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1237 if ($opt{no_usage} and $charged =~ /recur/) {
1238 $charged = "$charged - $usage_sql"
1245 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1247 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1248 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1249 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1255 '(' . $class->charged_sql(@_) .
1256 ' - ' . $class->paid_sql(@_) .
1257 ' - ' . $class->credited_sql(@_) . ')'
1260 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1262 Returns an SQL expression for the sum of payments applied to this item.
1267 my ($class, $start, $end, %opt) = @_;
1268 my $s = $start ? "AND cust_pay._date <= $start" : '';
1269 my $e = $end ? "AND cust_pay._date > $end" : '';
1270 my $setuprecur = $opt{setuprecur} || '';
1271 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1272 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1273 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1275 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1276 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1277 JOIN cust_pay USING (paynum)
1278 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1279 $s $e $setuprecur )";
1281 if ( $opt{no_usage} ) {
1282 # cap the amount paid at the sum of non-usage charges,
1283 # minus the amount credited against non-usage charges
1285 $class->charged_sql($start, $end, %opt) . ' - ' .
1286 $class->credited_sql($start, $end, %opt).')';
1295 my ($class, $start, $end, %opt) = @_;
1296 my $s = $start ? "AND cust_credit._date <= $start" : '';
1297 my $e = $end ? "AND cust_credit._date > $end" : '';
1298 my $setuprecur = $opt{setuprecur} || '';
1299 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1300 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1301 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1303 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1304 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1305 JOIN cust_credit USING (crednum)
1306 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1307 $s $e $setuprecur )";
1309 if ( $opt{no_usage} ) {
1310 # cap the amount credited at the sum of non-usage charges
1311 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1319 sub upgrade_tax_location {
1320 # For taxes that were calculated/invoiced before cust_location refactoring
1321 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1322 # they were calculated on a package-location basis. Create them here,
1323 # along with any necessary cust_location records and any tax exemption
1326 my ($class, %opt) = @_;
1327 # %opt may include 's' and 'e': start and end date ranges
1328 # and 'X': abort on any error, instead of just rolling back changes to
1331 my $oldAutoCommit = $FS::UID::AutoCommit;
1332 local $FS::UID::AutoCommit = 0;
1335 use FS::h_cust_main;
1336 use FS::h_cust_bill;
1338 use FS::h_cust_main_exemption;
1341 local $FS::cust_location::import = 1;
1343 my $conf = FS::Conf->new; # h_conf?
1344 return if $conf->config('tax_data_vendor'); #don't touch this case
1345 my $use_ship = $conf->exists('tax-ship_address');
1346 my $use_pkgloc = $conf->exists('tax-pkg_address');
1348 my $date_where = '';
1350 $date_where .= " AND cust_bill._date >= $opt{s}";
1353 $date_where .= " AND cust_bill._date < $opt{e}";
1356 my $commit_each_invoice = 1 unless $opt{X};
1358 # if an invoice has either of these kinds of objects, then it doesn't
1359 # need to be upgraded...probably
1360 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1361 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1362 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1363 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1364 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1365 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1366 ' AND exempt_monthly IS NULL';
1368 my %all_tax_names = (
1371 map { $_->taxname => 1 }
1372 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1375 my $search = FS::Cursor->new({
1376 table => 'cust_bill',
1378 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1379 "AND NOT EXISTS($sub_has_exempt) ".
1383 #print "Processing ".scalar(@invnums)." invoices...\n";
1387 while (my $cust_bill = $search->fetch) {
1388 my $invnum = $cust_bill->invnum;
1390 print STDERR "Invoice #$invnum\n";
1392 my %pkgpart_taxclass; # pkgpart => taxclass
1393 my %pkgpart_exempt_setup;
1394 my %pkgpart_exempt_recur;
1395 my $h_cust_bill = qsearchs('h_cust_bill',
1396 { invnum => $invnum,
1397 history_action => 'insert' });
1398 if (!$h_cust_bill) {
1399 warn "no insert record for invoice $invnum; skipped\n";
1400 #$date = $cust_bill->_date as a fallback?
1401 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1402 # when looking up history records in other tables.
1405 my $custnum = $h_cust_bill->custnum;
1407 # Determine the address corresponding to this tax region.
1408 # It's either the bill or ship address of the customer as of the
1409 # invoice date-of-insertion. (Not necessarily the invoice date.)
1410 my $date = $h_cust_bill->history_date;
1411 local($FS::Record::qsearch_qualify_columns) = 0;
1412 my $h_cust_main = qsearchs('h_cust_main',
1413 { custnum => $custnum },
1414 FS::h_cust_main->sql_h_searchs($date)
1416 if (!$h_cust_main ) {
1417 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1419 # fallback to current $cust_main? sounds dangerous.
1422 # This is a historical customer record, so it has a historical address.
1423 # If there's no cust_location matching this custnum and address (there
1424 # probably isn't), create one.
1425 my %tax_loc; # keys are pkgnums, values are cust_location objects
1426 my $default_tax_loc;
1427 if ( $h_cust_main->bill_locationnum ) {
1428 # the location has already been upgraded
1430 $default_tax_loc = $h_cust_main->ship_location;
1432 $default_tax_loc = $h_cust_main->bill_location;
1435 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1436 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1437 FS::cust_main->location_fields;
1438 # not really needed for this, and often result in duplicate locations
1439 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1441 $hash{custnum} = $h_cust_main->custnum;
1442 $default_tax_loc = FS::cust_location->new(\%hash);
1443 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1445 warn "couldn't create historical location record for cust#".
1446 $h_cust_main->custnum.": $error\n";
1451 $exempt_cust = 1 if $h_cust_main->tax;
1453 # classify line items
1455 my %nontax_items; # taxclass => array of cust_bill_pkg
1456 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1457 my $pkgnum = $item->pkgnum;
1459 if ( $pkgnum == 0 ) {
1461 push @tax_items, $item;
1464 # (pkgparts really shouldn't change, right?)
1465 local($FS::Record::qsearch_qualify_columns) = 0;
1466 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1467 FS::h_cust_pkg->sql_h_searchs($date)
1469 if ( !$h_cust_pkg ) {
1470 warn "no historical package #".$item->pkgpart."; skipped\n";
1473 my $pkgpart = $h_cust_pkg->pkgpart;
1475 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1476 # then this package already had a locationnum assigned, and that's
1477 # the one to use for tax calculation
1478 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1480 # use the customer's bill or ship loc, which was inserted earlier
1481 $tax_loc{$pkgnum} = $default_tax_loc;
1484 if (!exists $pkgpart_taxclass{$pkgpart}) {
1485 local($FS::Record::qsearch_qualify_columns) = 0;
1486 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1487 FS::h_part_pkg->sql_h_searchs($date)
1489 if ( !$h_part_pkg ) {
1490 warn "no historical package def #$pkgpart; skipped\n";
1493 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1494 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1495 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1498 # mark any exemptions that apply
1499 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1500 $item->set('exempt_setup' => 1);
1503 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1504 $item->set('exempt_recur' => 1);
1507 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1509 $nontax_items{$taxclass} ||= [];
1510 push @{ $nontax_items{$taxclass} }, $item;
1514 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1517 # Get any per-customer taxname exemptions that were in effect.
1518 my %exempt_cust_taxname;
1519 foreach (keys %all_tax_names) {
1520 local($FS::Record::qsearch_qualify_columns) = 0;
1521 my $h_exemption = qsearchs('h_cust_main_exemption', {
1522 'custnum' => $custnum,
1525 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1528 $exempt_cust_taxname{ $_ } = 1;
1532 # Use a variation on the procedure in
1533 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1535 my @loc_keys = qw( district city county state country );
1536 my %taxdef_by_name; # by name, and then by taxclass
1537 my %est_tax; # by name, and then by taxclass
1538 my %taxable_items; # by taxnum, and then an array
1540 foreach my $taxclass (keys %nontax_items) {
1541 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1542 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1543 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1544 my @elim = qw( district city county state );
1545 my @taxdefs; # because there may be several with different taxnames
1547 $myhash{taxclass} = $taxclass;
1548 @taxdefs = qsearch('cust_main_county', \%myhash);
1550 $myhash{taxclass} = '';
1551 @taxdefs = qsearch('cust_main_county', \%myhash);
1553 $myhash{ shift @elim } = '';
1554 } while scalar(@elim) and !@taxdefs;
1556 foreach my $taxdef (@taxdefs) {
1557 next if $taxdef->tax == 0;
1558 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1560 $taxable_items{$taxdef->taxnum} ||= [];
1561 # clone the item so that taxdef-dependent changes don't
1562 # change it for other taxdefs
1563 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1565 # these flags are already set if the part_pkg declares itself exempt
1566 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1567 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1570 my $taxable = $item->setup + $item->recur;
1572 # h_cust_credit_bill_pkg?
1573 # NO. Because if these exemptions HAD been created at the time of
1574 # billing, and then a credit applied later, the exemption would
1575 # have been adjusted by the amount of the credit. So we adjust
1576 # the taxable amount before creating the exemption.
1577 # But don't deduct the credit from taxable, because the tax was
1578 # calculated before the credit was applied.
1579 foreach my $f (qw(setup recur)) {
1580 my $credited = FS::Record->scalar_sql(
1581 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1582 "WHERE billpkgnum = ? AND setuprecur = ?",
1586 $item->set($f, $item->get($f) - $credited) if $credited;
1588 my $existing_exempt = FS::Record->scalar_sql(
1589 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1590 "billpkgnum = ? AND taxnum = ?",
1591 $item->billpkgnum, $taxdef->taxnum
1593 $taxable -= $existing_exempt;
1595 if ( $taxable and $exempt_cust ) {
1596 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1599 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1600 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1603 if ( $taxable and $item->exempt_setup ) {
1604 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1605 $taxable -= $item->setup;
1607 if ( $taxable and $item->exempt_recur ) {
1608 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1609 $taxable -= $item->recur;
1612 $item->set('taxable' => $taxable);
1613 push @{ $taxable_items{$taxdef->taxnum} }, $item
1616 # estimate the amount of tax (this is necessary because different
1617 # taxdefs with the same taxname may have different tax rates)
1618 # and sum that for each taxname/taxclass combination
1620 $est_tax{$taxdef->taxname} ||= {};
1621 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1622 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1623 $taxable * $taxdef->tax;
1625 foreach (@new_exempt) {
1626 next if $_->{amount} == 0;
1627 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1629 billpkgnum => $item->billpkgnum,
1630 taxnum => $taxdef->taxnum,
1632 my $error = $cust_tax_exempt_pkg->insert;
1634 my $pkgnum = $item->pkgnum;
1635 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1639 } #foreach @new_exempt
1642 } #foreach $taxclass
1644 # Now go through the billed taxes and match them up with the line items.
1645 TAX_ITEM: foreach my $tax_item ( @tax_items )
1647 my $taxname = $tax_item->itemdesc;
1648 $taxname = '' if $taxname eq 'Tax';
1650 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1651 # then we didn't find any applicable taxes with this name
1652 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1653 # possibly all of these should be "next TAX_ITEM", but whole invoices
1654 # are transaction protected and we can go back and retry them.
1657 # classname => cust_main_county
1658 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1660 # Divide the tax item among taxclasses, if necessary
1661 # classname => estimated tax amount
1662 my $this_est_tax = $est_tax{$taxname};
1663 if (!defined $this_est_tax) {
1664 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1667 my $est_total = sum(values %$this_est_tax);
1668 if ( $est_total == 0 ) {
1670 warn "estimated tax on invoice #$invnum is zero.\n";
1674 my $real_tax = $tax_item->setup;
1675 printf ("Distributing \$%.2f tax:\n", $real_tax);
1676 my $cents_remaining = $real_tax * 100; # for rounding error
1677 my @tax_links; # partial CBPTL hashrefs
1678 foreach my $taxclass (keys %taxdef_by_class) {
1679 my $taxdef = $taxdef_by_class{$taxclass};
1680 # these items already have "taxable" set to their charge amount
1681 # after applying any credits or exemptions
1682 my @items = @{ $taxable_items{$taxdef->taxnum} };
1683 my $subtotal = sum(map {$_->get('taxable')} @items);
1684 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1686 foreach my $nontax (@items) {
1687 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1688 my $part = int($real_tax
1690 * ($this_est_tax->{$taxclass}/$est_total)
1692 * ($nontax->get('taxable'))/$subtotal
1696 $cents_remaining -= $part;
1698 taxnum => $taxdef->taxnum,
1699 pkgnum => $nontax->pkgnum,
1700 locationnum => $my_tax_loc->locationnum,
1701 billpkgnum => $nontax->billpkgnum,
1705 } #foreach $taxclass
1706 # Distribute any leftover tax round-robin style, one cent at a time.
1708 my $nlinks = scalar(@tax_links);
1710 # ensure that it really is an integer
1711 $cents_remaining = sprintf('%.0f', $cents_remaining);
1712 while ($cents_remaining > 0) {
1713 $tax_links[$i % $nlinks]->{cents} += 1;
1718 warn "Can't create tax links--no taxable items found.\n";
1722 # Gather credit/payment applications so that we can link them
1725 qsearch( 'cust_credit_bill_pkg',
1726 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1728 qsearch( 'cust_bill_pay_pkg',
1729 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1733 # grab the first one
1734 my $this_unlinked = shift @unlinked;
1735 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1737 # Create tax links (yay!)
1738 printf("Creating %d tax links.\n",scalar(@tax_links));
1739 foreach (@tax_links) {
1740 my $link = FS::cust_bill_pkg_tax_location->new({
1741 billpkgnum => $tax_item->billpkgnum,
1742 taxtype => 'FS::cust_main_county',
1743 locationnum => $_->{locationnum},
1744 taxnum => $_->{taxnum},
1745 pkgnum => $_->{pkgnum},
1746 amount => sprintf('%.2f', $_->{cents} / 100),
1747 taxable_billpkgnum => $_->{billpkgnum},
1749 my $error = $link->insert;
1751 warn "Can't create tax link for inv#$invnum: $error\n";
1755 my $link_cents = $_->{cents};
1756 # update/create subitem links
1758 # If $this_unlinked is undef, then we've allocated all of the
1759 # credit/payment applications to the tax item. If $link_cents is 0,
1760 # then we've applied credits/payments to all of this package fraction,
1761 # so go on to the next.
1762 while ($this_unlinked and $link_cents) {
1763 # apply as much as possible of $link_amount to this credit/payment
1765 my $apply_cents = min($link_cents, $unlinked_cents);
1766 $link_cents -= $apply_cents;
1767 $unlinked_cents -= $apply_cents;
1768 # $link_cents or $unlinked_cents or both are now zero
1769 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1770 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1771 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1772 if ( $this_unlinked->$pkey ) {
1773 # then it's an existing link--replace it
1774 $error = $this_unlinked->replace;
1776 $this_unlinked->insert;
1778 # what do we do with errors at this stage?
1780 warn "Error creating tax application link: $error\n";
1781 next INVOICE; # for lack of a better idea
1784 if ( $unlinked_cents == 0 ) {
1785 # then we've allocated all of this payment/credit application,
1786 # so grab the next one
1787 $this_unlinked = shift @unlinked;
1788 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1789 } elsif ( $link_cents == 0 ) {
1790 # then we've covered all of this package tax fraction, so split
1791 # off a new application from this one
1792 $this_unlinked = $this_unlinked->new({
1793 $this_unlinked->hash,
1796 # $unlinked_cents is still what it is
1799 } #while $this_unlinked and $link_cents
1800 } #foreach (@tax_links)
1801 } #foreach $tax_item
1803 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1809 $dbh->rollback if $oldAutoCommit;
1810 die "Upgrade halted.\n" unless $commit_each_invoice;
1814 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1819 # Return an array of hashrefs for each cust_bill_pkg_tax_location
1820 # applied to this bill for this cust_bill_pkg.pkgnum.
1823 # In some situations, this list will contain more tax records than the
1824 # ones directly related to $self->billpkgnum. The returned list contains
1825 # all records, for this bill, charged against this billpkgnum's pkgnum.
1827 # One must keep this in mind when using data returned by this method.
1829 # An unaddressed deficiency in the cust_bill_pkg_tax_location model makes
1830 # this necessary: When a linked-hidden package generates a tax/fee as a row
1831 # in cust_bill_pkg_tax_location, there is not enough information to surmise
1832 # with specificity which billpkgnum row represents the direct parent of the
1833 # the linked-hidden package's tax row. The closest we can get to this
1834 # backwards reassociation is to use the pkgnum. Therefore, when multiple
1835 # billpkgnum's appear with the same pkgnum, this method is going to return
1836 # the tax records for ALL of those billpkgnum's, not just $self->billpkgnum.
1838 # This could be addressed with an update to the model, and to the billing
1839 # routine that generates rows into cust_bill_pkg_tax_location. Perhaps a
1840 # column, link_billpkgnum or parent_billpkgnum, recording the link. I'm not
1841 # doing that now, because there would be no possible repair of data stored
1842 # historically prior to such a fix. I need _pkg_tax_list() to not be
1843 # broken for already-generated bills.
1845 # Any code you write relying on _pkg_tax_list() MUST be aware of, and
1846 # account for, the possible return of duplicated tax records returned
1847 # when method is called on multiple cust_bill_pkg_tax_location rows.
1848 # Duplicates can be identified by billpkgtaxlocationnum column.
1852 my $search_selector;
1853 if ( $self->pkgnum ) {
1855 # For taxes applied to normal billing items
1857 ' cust_bill_pkg_tax_location.pkgnum = '
1858 . dbh->quote( $self->pkgnum );
1860 } elsif ( $self->feepart ) {
1862 # For taxes applied to fees, when the fee is not attached to a package
1863 # i.e. late fees, billing events fees
1865 ' cust_bill_pkg_tax_location.taxable_billpkgnum = '
1866 . dbh->quote( $self->billpkgnum );
1869 warn "_pkg_tax_list() unhandled case breaking taxes into sections";
1870 warn "_pkg_tax_list() $_: ".$self->$_
1871 for qw(pkgnum billpkgnum feepart);
1876 billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
1877 billpkgnum => $_->billpkgnum,
1878 taxnum => $_->taxnum,
1879 amount => $_->amount,
1880 taxname => $_->taxname,
1883 table => 'cust_bill_pkg_tax_location',
1885 LEFT JOIN cust_bill_pkg
1886 ON cust_bill_pkg.billpkgnum
1887 = cust_bill_pkg_tax_location.taxable_billpkgnum
1889 select => join( ', ', (qw|
1890 cust_bill_pkg.billpkgnum
1891 cust_bill_pkg_tax_location.billpkgtaxlocationnum
1892 cust_bill_pkg_tax_location.taxnum
1893 cust_bill_pkg_tax_location.amount
1897 ' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
1905 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1909 use Date::Parse 'str2time';
1912 my $upgrade = 'tax_location_2012';
1913 return if FS::upgrade_journal->is_done($upgrade);
1914 my $job = FS::queue->new({
1915 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1917 # call it kind of like a class method, not that it matters much
1918 $job->insert($class, 's' => str2time('2012-01-01'));
1919 # if there's a customer location upgrade queued also, wait for it to
1921 my $location_job = qsearchs('queue', {
1922 job => 'FS::cust_main::Location::process_upgrade_location'
1924 if ( $location_job ) {
1925 $job->depend_insert($location_job->jobnum);
1927 # Then mark the upgrade as done, so that we don't queue the job twice
1928 # and somehow run two of them concurrently.
1929 FS::upgrade_journal->set_done($upgrade);
1930 # This upgrade now does the job of assigning taxable_billpkgnums to
1931 # cust_bill_pkg_tax_location, so set that task done also.
1932 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1939 setup and recur shouldn't be separate fields. There should be one "amount"
1940 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1942 A line item with both should really be two separate records (preserving
1943 sdate and edate for setup fees for recurring packages - that information may
1944 be valuable later). Invoice generation (cust_main::bill), invoice printing
1945 (cust_bill), tax reports (report_tax.cgi) and line item reports
1946 (cust_bill_pkg.cgi) would need to be updated.
1948 owed_setup and owed_recur could then be repaced by just owed, and
1949 cust_bill::open_cust_bill_pkg and
1950 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1952 The upgrade procedure is pretty sketchy.
1956 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1957 from the base documentation.