1 package FS::cust_bill_pkg;
2 use base qw( FS::TemplateItem_Mixin FS::cust_main_Mixin FS::Record );
5 use vars qw( @ISA $DEBUG $me );
7 use List::Util qw( sum min );
9 use FS::Record qw( qsearch qsearchs dbh );
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pkg_discount;
14 use FS::cust_bill_pkg_fee;
15 use FS::cust_bill_pay_pkg;
16 use FS::cust_credit_bill_pkg;
17 use FS::cust_tax_exempt_pkg;
18 use FS::cust_bill_pkg_tax_location;
19 use FS::cust_bill_pkg_tax_rate_location;
20 use FS::cust_tax_adjustment;
21 use FS::cust_bill_pkg_void;
22 use FS::cust_bill_pkg_detail_void;
23 use FS::cust_bill_pkg_display_void;
24 use FS::cust_bill_pkg_discount_void;
25 use FS::cust_bill_pkg_tax_location_void;
26 use FS::cust_bill_pkg_tax_rate_location_void;
27 use FS::cust_tax_exempt_pkg_void;
28 use FS::cust_bill_pkg_fee_void;
33 $me = '[FS::cust_bill_pkg]';
37 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
41 use FS::cust_bill_pkg;
43 $record = new FS::cust_bill_pkg \%hash;
44 $record = new FS::cust_bill_pkg { 'column' => 'value' };
46 $error = $record->insert;
48 $error = $record->check;
52 An FS::cust_bill_pkg object represents an invoice line item.
53 FS::cust_bill_pkg inherits from FS::Record. The following fields are
64 invoice (see L<FS::cust_bill>)
68 package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line)
70 =item pkgpart_override
72 optional package definition (see L<FS::part_pkg>) override
84 starting date of recurring fee
88 ending date of recurring fee
92 Line item description (overrides normal package description)
96 If not set, defaults to 1
100 If not set, defaults to setup
104 If not set, defaults to recur
108 If set to Y, indicates data should not appear as separate line item on invoice
112 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">. Also
113 see L<Time::Local> and L<Date::Parse> for conversion functions.
121 Creates a new line item. To add the line item to the database, see
122 L<"insert">. Line items are normally created by calling the bill method of a
123 customer object (see L<FS::cust_main>).
127 sub table { 'cust_bill_pkg'; }
129 sub detail_table { 'cust_bill_pkg_detail'; }
130 sub display_table { 'cust_bill_pkg_display'; }
131 sub discount_table { 'cust_bill_pkg_discount'; }
132 #sub tax_location_table { 'cust_bill_pkg_tax_location'; }
133 #sub tax_rate_location_table { 'cust_bill_pkg_tax_rate_location'; }
134 #sub tax_exempt_pkg_table { 'cust_tax_exempt_pkg'; }
138 Adds this line item to the database. If there is an error, returns the error,
139 otherwise returns false.
146 local $SIG{HUP} = 'IGNORE';
147 local $SIG{INT} = 'IGNORE';
148 local $SIG{QUIT} = 'IGNORE';
149 local $SIG{TERM} = 'IGNORE';
150 local $SIG{TSTP} = 'IGNORE';
151 local $SIG{PIPE} = 'IGNORE';
153 my $oldAutoCommit = $FS::UID::AutoCommit;
154 local $FS::UID::AutoCommit = 0;
157 my $error = $self->SUPER::insert;
159 $dbh->rollback if $oldAutoCommit;
163 if ( $self->get('details') ) {
164 foreach my $detail ( @{$self->get('details')} ) {
165 $detail->billpkgnum($self->billpkgnum);
166 $error = $detail->insert;
168 $dbh->rollback if $oldAutoCommit;
169 return "error inserting cust_bill_pkg_detail: $error";
174 if ( $self->get('display') ) {
175 foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
176 $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
177 $error = $cust_bill_pkg_display->insert;
179 $dbh->rollback if $oldAutoCommit;
180 return "error inserting cust_bill_pkg_display: $error";
185 if ( $self->get('discounts') ) {
186 foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
187 $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
188 $error = $cust_bill_pkg_discount->insert;
190 $dbh->rollback if $oldAutoCommit;
191 return "error inserting cust_bill_pkg_discount: $error";
196 foreach my $cust_tax_exempt_pkg ( @{$self->cust_tax_exempt_pkg} ) {
197 $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
198 $error = $cust_tax_exempt_pkg->insert;
200 $dbh->rollback if $oldAutoCommit;
201 return "error inserting cust_tax_exempt_pkg: $error";
205 my $tax_location = $self->get('cust_bill_pkg_tax_location');
206 if ( $tax_location ) {
207 foreach my $link ( @$tax_location ) {
208 next if $link->billpkgtaxlocationnum; # don't try to double-insert
209 # This cust_bill_pkg can be linked on either side (i.e. it can be the
210 # tax or the taxed item). If the other side is already inserted,
211 # then set billpkgnum to ours, and insert the link. Otherwise,
212 # set billpkgnum to ours and pass the link off to the cust_bill_pkg
213 # on the other side, to be inserted later.
215 my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
216 if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
217 $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
218 # break circular links when doing this
219 $link->set('tax_cust_bill_pkg', '');
221 my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
222 if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
223 $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
224 # XXX if we ever do tax-on-tax for these, this will have to change
225 # since pkgnum will be zero
226 $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
227 $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
228 $link->set('taxable_cust_bill_pkg', '');
231 if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
232 $error = $link->insert;
234 $dbh->rollback if $oldAutoCommit;
235 return "error inserting cust_bill_pkg_tax_location: $error";
239 $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
240 : $link->get('tax_cust_bill_pkg');
241 my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
242 push @$link_array, $link;
243 $other->set('cust_bill_pkg_tax_location' => $link_array);
248 # someday you will be as awesome as cust_bill_pkg_tax_location...
250 my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
251 if ( $tax_rate_location ) {
252 foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
253 $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
254 $error = $cust_bill_pkg_tax_rate_location->insert;
256 $dbh->rollback if $oldAutoCommit;
257 return "error inserting cust_bill_pkg_tax_rate_location: $error";
262 my $fee_links = $self->get('cust_bill_pkg_fee');
264 foreach my $link ( @$fee_links ) {
265 # very similar to cust_bill_pkg_tax_location, for obvious reasons
266 next if $link->billpkgfeenum; # don't try to double-insert
268 my $target = $link->get('cust_bill_pkg'); # the line item of the fee
269 my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
271 if ( $target and $target->billpkgnum ) {
272 $link->set('billpkgnum', $target->billpkgnum);
273 # base_invnum => null indicates that the fee is based on its own
275 $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
276 $link->set('cust_bill_pkg', '');
279 if ( $base and $base->billpkgnum ) {
280 $link->set('base_billpkgnum', $base->billpkgnum);
281 $link->set('base_cust_bill_pkg', '');
283 # it's based on a line item that's not yet inserted
284 my $link_array = $base->get('cust_bill_pkg_fee') || [];
285 push @$link_array, $link;
286 $base->set('cust_bill_pkg_fee' => $link_array);
287 next; # don't insert the link yet
290 $error = $link->insert;
292 $dbh->rollback if $oldAutoCommit;
293 return "error inserting cust_bill_pkg_fee: $error";
298 if ( my $fee_origin = $self->get('fee_origin') ) {
299 $fee_origin->set('billpkgnum' => $self->billpkgnum);
300 $error = $fee_origin->replace;
302 $dbh->rollback if $oldAutoCommit;
303 return "error updating fee origin record: $error";
307 my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
308 if ( $cust_tax_adjustment ) {
309 $cust_tax_adjustment->billpkgnum($self->billpkgnum);
310 $error = $cust_tax_adjustment->replace;
312 $dbh->rollback if $oldAutoCommit;
313 return "error replacing cust_tax_adjustment: $error";
317 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
324 Voids this line item: deletes the line item and adds a record of the voided
325 line item to the FS::cust_bill_pkg_void table (and related tables).
331 my $reason = scalar(@_) ? shift : '';
333 local $SIG{HUP} = 'IGNORE';
334 local $SIG{INT} = 'IGNORE';
335 local $SIG{QUIT} = 'IGNORE';
336 local $SIG{TERM} = 'IGNORE';
337 local $SIG{TSTP} = 'IGNORE';
338 local $SIG{PIPE} = 'IGNORE';
340 my $oldAutoCommit = $FS::UID::AutoCommit;
341 local $FS::UID::AutoCommit = 0;
344 my $cust_bill_pkg_void = new FS::cust_bill_pkg_void ( {
345 map { $_ => $self->get($_) } $self->fields
347 $cust_bill_pkg_void->reason($reason);
348 my $error = $cust_bill_pkg_void->insert;
350 $dbh->rollback if $oldAutoCommit;
354 foreach my $table (qw(
356 cust_bill_pkg_display
357 cust_bill_pkg_discount
358 cust_bill_pkg_tax_location
359 cust_bill_pkg_tax_rate_location
364 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
366 my $vclass = 'FS::'.$table.'_void';
367 my $void = $vclass->new( {
368 map { $_ => $linked->get($_) } $linked->fields
370 my $error = $void->insert || $linked->delete;
372 $dbh->rollback if $oldAutoCommit;
380 $error = $self->delete;
382 $dbh->rollback if $oldAutoCommit;
386 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
401 local $SIG{HUP} = 'IGNORE';
402 local $SIG{INT} = 'IGNORE';
403 local $SIG{QUIT} = 'IGNORE';
404 local $SIG{TERM} = 'IGNORE';
405 local $SIG{TSTP} = 'IGNORE';
406 local $SIG{PIPE} = 'IGNORE';
408 my $oldAutoCommit = $FS::UID::AutoCommit;
409 local $FS::UID::AutoCommit = 0;
412 foreach my $table (qw(
414 cust_bill_pkg_display
415 cust_bill_pkg_discount
416 cust_bill_pkg_tax_location
417 cust_bill_pkg_tax_rate_location
424 foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
425 my $error = $linked->delete;
427 $dbh->rollback if $oldAutoCommit;
434 foreach my $cust_tax_adjustment (
435 qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
437 $cust_tax_adjustment->billpkgnum(''); #NULL
438 my $error = $cust_tax_adjustment->replace;
440 $dbh->rollback if $oldAutoCommit;
445 my $error = $self->SUPER::delete(@_);
447 $dbh->rollback if $oldAutoCommit;
451 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
457 #alas, bin/follow-tax-rename
459 #=item replace OLD_RECORD
461 #Currently unimplemented. This would be even more of an accounting nightmare
462 #than deleteing the items. Just don't do it.
467 # return "Can't modify cust_bill_pkg records!";
472 Checks all fields to make sure this is a valid line item. If there is an
473 error, returns the error, otherwise returns false. Called by the insert
482 $self->ut_numbern('billpkgnum')
483 || $self->ut_snumber('pkgnum')
484 || $self->ut_number('invnum')
485 || $self->ut_money('setup')
486 || $self->ut_moneyn('unitsetup')
487 || $self->ut_currencyn('setup_billed_currency')
488 || $self->ut_moneyn('setup_billed_amount')
489 || $self->ut_money('recur')
490 || $self->ut_moneyn('unitrecur')
491 || $self->ut_currencyn('recur_billed_currency')
492 || $self->ut_moneyn('recur_billed_amount')
493 || $self->ut_numbern('sdate')
494 || $self->ut_numbern('edate')
495 || $self->ut_textn('itemdesc')
496 || $self->ut_textn('itemcomment')
497 || $self->ut_enum('hidden', [ '', 'Y' ])
499 return $error if $error;
501 $self->regularize_details;
503 #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
504 if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
505 return "Unknown pkgnum ". $self->pkgnum
506 unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
509 return "Unknown invnum"
510 unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
515 =item regularize_details
517 Converts the contents of the 'details' pseudo-field to
518 L<FS::cust_bill_pkg_detail> objects, if they aren't already.
522 sub regularize_details {
524 if ( $self->get('details') ) {
525 foreach my $detail ( @{$self->get('details')} ) {
526 if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
527 # then turn it into one
529 if ( ! ref($detail) ) {
530 $hash{'detail'} = $detail;
532 elsif ( ref($detail) eq 'HASH' ) {
535 elsif ( ref($detail) eq 'ARRAY' ) {
536 carp "passing invoice details as arrays is deprecated";
537 #carp "this way sucks, use a hash"; #but more useful/friendly
538 $hash{'format'} = $detail->[0];
539 $hash{'detail'} = $detail->[1];
540 $hash{'amount'} = $detail->[2];
541 $hash{'classnum'} = $detail->[3];
542 $hash{'phonenum'} = $detail->[4];
543 $hash{'accountcode'} = $detail->[5];
544 $hash{'startdate'} = $detail->[6];
545 $hash{'duration'} = $detail->[7];
546 $hash{'regionname'} = $detail->[8];
549 die "unknown detail type ". ref($detail);
551 $detail = new FS::cust_bill_pkg_detail \%hash;
553 $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
561 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
565 Returns the customer (L<FS::cust_main> object) for this line item.
570 # required for cust_main_Mixin equivalence
571 # and use cust_bill instead of cust_pkg because this might not have a
574 my $cust_bill = $self->cust_bill or return '';
575 $cust_bill->cust_main;
578 =item previous_cust_bill_pkg
580 Returns the previous cust_bill_pkg for this package, if any.
584 sub previous_cust_bill_pkg {
586 return unless $self->sdate;
588 'table' => 'cust_bill_pkg',
589 'hashref' => { 'pkgnum' => $self->pkgnum,
590 'sdate' => { op=>'<', value=>$self->sdate },
592 'order_by' => 'ORDER BY sdate DESC LIMIT 1',
598 Returns the amount owed (still outstanding) on this line item's setup fee,
599 which is the amount of the line item minus all payment applications (see
600 L<FS::cust_bill_pay_pkg> and credit applications (see
601 L<FS::cust_credit_bill_pkg>).
607 $self->owed('setup', @_);
612 Returns the amount owed (still outstanding) on this line item's recurring fee,
613 which is the amount of the line item minus all payment applications (see
614 L<FS::cust_bill_pay_pkg> and credit applications (see
615 L<FS::cust_credit_bill_pkg>).
621 $self->owed('recur', @_);
624 # modeled after cust_bill::owed...
626 my( $self, $field ) = @_;
627 my $balance = $self->$field();
628 $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
629 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
630 $balance = sprintf( '%.2f', $balance );
631 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
637 my( $self, $field ) = @_;
638 my $balance = $self->$field();
639 $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
640 $balance = sprintf( '%.2f', $balance );
641 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
645 sub cust_bill_pay_pkg {
646 my( $self, $field ) = @_;
647 qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
648 'setuprecur' => $field,
653 sub cust_credit_bill_pkg {
654 my( $self, $field ) = @_;
655 qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
656 'setuprecur' => $field,
663 Returns the number of billing units (for tax purposes) represented by this,
670 $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
675 If this item has any discounts, returns a hashref in the format used
676 by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
677 on an invoice. This will contain the keys 'description', 'amount',
678 'ext_description' (an arrayref of text lines describing the discounts),
679 and '_is_discount' (a flag).
681 The value for 'amount' will be negative, and will be scaled for the package
688 my @pkg_discounts = $self->pkg_discount;
689 return if @pkg_discounts == 0;
690 # special case: if there are old "discount details" on this line item, don't
691 # show discount line items
692 if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
699 description => $self->mt('Discount'),
701 ext_description => \@ext,
702 # maybe should show quantity/unit discount?
704 foreach my $pkg_discount (@pkg_discounts) {
705 push @ext, $pkg_discount->description;
706 $d->{amount} -= $pkg_discount->amount;
708 $d->{amount} *= $self->quantity || 1;
713 =item set_display OPTION => VALUE ...
715 A helper method for I<insert>, populates the pseudo-field B<display> with
716 appropriate FS::cust_bill_pkg_display objects.
718 Options are passed as a list of name/value pairs. Options are:
720 part_pkg: FS::part_pkg object from this line item's package.
722 real_pkgpart: if this line item comes from a bundled package, the pkgpart
723 of the owning package. Otherwise the same as the part_pkg's pkgpart above.
728 my( $self, %opt ) = @_;
729 my $part_pkg = $opt{'part_pkg'};
730 my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
732 my $conf = new FS::Conf;
734 # whether to break this down into setup/recur/usage
735 my $separate = $conf->exists('separate_usage');
737 my $usage_mandate = $part_pkg->option('usage_mandate', 'Hush!')
738 || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
740 # or use the category from $opt{'part_pkg'} if its not bundled?
741 my $categoryname = $cust_pkg->part_pkg->categoryname;
743 # if we don't have to separate setup/recur/usage, or put this in a
744 # package-specific section, or display a usage summary, then don't
745 # even create one of these. The item will just display in the unnamed
746 # section as a single line plus details.
747 return $self->set('display', [])
748 unless $separate || $categoryname || $usage_mandate;
752 my %hash = ( 'section' => $categoryname );
754 # whether to put usage details in a separate section, and if so, which one
755 my $usage_section = $part_pkg->option('usage_section', 'Hush!')
756 || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
758 # whether to show a usage summary line (total usage charges, no details)
759 my $summary = $part_pkg->option('summarize_usage', 'Hush!')
760 || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
763 # create lines for setup and (non-usage) recur, in the main section
764 push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
765 push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
767 # display everything in a single line
768 push @display, new FS::cust_bill_pkg_display
771 # and if usage_mandate is enabled, hide details
772 # (this only works on multisection invoices...)
773 ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
777 if ($separate && $usage_section && $summary) {
778 # create a line for the usage summary in the main section
779 push @display, new FS::cust_bill_pkg_display { type => 'U',
785 if ($usage_mandate || ($usage_section && $summary) ) {
786 $hash{post_total} = 'Y';
789 if ($separate || $usage_mandate) {
790 # show call details for this line item in the usage section.
791 # if usage_mandate is on, this will display below the section subtotal.
792 # this also happens if usage is in a separate section and there's a
793 # summary in the main section, though I'm not sure why.
794 $hash{section} = $usage_section if $usage_section;
795 push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
798 $self->set('display', \@display);
804 Returns a hash: keys are "setup", "recur" or usage classnum, values are
805 FS::cust_bill_pkg objects, each with no more than a single class (setup or
812 # XXX this goes away with cust_bill_pkg refactor
814 my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
815 my %cust_bill_pkg = ();
817 $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
818 $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
821 #split setup and recur
822 if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
823 my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
824 $cust_bill_pkg->set('details', []);
825 $cust_bill_pkg->recur(0);
826 $cust_bill_pkg->unitrecur(0);
827 $cust_bill_pkg->type('');
828 $cust_bill_pkg_recur->setup(0);
829 $cust_bill_pkg_recur->unitsetup(0);
830 $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
834 #split usage from recur
835 my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
836 if exists($cust_bill_pkg{recur});
837 warn "usage is $usage\n" if $DEBUG > 1;
839 my $cust_bill_pkg_usage =
840 new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
841 $cust_bill_pkg_usage->recur( $usage );
842 $cust_bill_pkg_usage->type( 'U' );
843 my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
844 $cust_bill_pkg{recur}->recur( $recur );
845 $cust_bill_pkg{recur}->type( '' );
846 $cust_bill_pkg{recur}->set('details', []);
847 $cust_bill_pkg{''} = $cust_bill_pkg_usage;
850 #subdivide usage by usage_class
851 if (exists($cust_bill_pkg{''})) {
852 foreach my $class (grep { $_ } $self->usage_classes) {
853 my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
854 my $cust_bill_pkg_usage =
855 new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
856 $cust_bill_pkg_usage->recur( $usage );
857 $cust_bill_pkg_usage->set('details', []);
858 my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
859 $cust_bill_pkg{''}->recur( $classless );
860 $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
862 warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
863 if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
864 delete $cust_bill_pkg{''}
865 unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
868 # # sort setup,recur,'', and the rest numeric && return
869 # my @result = map { $cust_bill_pkg{$_} }
870 # sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
871 # ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
873 # keys %cust_bill_pkg;
882 Returns the amount of the charge associated with usage class CLASSNUM if
883 CLASSNUM is defined. Otherwise returns the total charge associated with
889 my( $self, $classnum ) = @_;
890 $self->regularize_details;
892 if ( $self->get('details') ) {
895 map { $_->amount || 0 }
896 grep { !defined($classnum) or $classnum eq $_->classnum }
897 @{ $self->get('details') }
902 my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
903 ' WHERE billpkgnum = '. $self->billpkgnum;
904 if (defined $classnum) {
905 if ($classnum =~ /^(\d+)$/) {
906 $sql .= " AND classnum = $1";
907 } elsif ($classnum eq '') {
908 $sql .= " AND classnum IS NULL";
912 my $sth = dbh->prepare($sql) or die dbh->errstr;
913 $sth->execute or die $sth->errstr;
915 return $sth->fetchrow_arrayref->[0] || 0;
923 Returns a list of usage classnums associated with this invoice line's
930 $self->regularize_details;
932 if ( $self->get('details') ) {
934 my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
940 qsearch({ table => 'cust_bill_pkg_detail',
941 hashref => { billpkgnum => $self->billpkgnum },
942 select => 'DISTINCT classnum',
949 sub cust_tax_exempt_pkg {
952 $self->{Hash}->{cust_tax_exempt_pkg} ||= [];
955 =item cust_bill_pkg_tax_Xlocation
957 Returns the list of associated cust_bill_pkg_tax_location and/or
958 cust_bill_pkg_tax_rate_location objects
962 sub cust_bill_pkg_tax_Xlocation {
965 my %hash = ( 'billpkgnum' => $self->billpkgnum );
968 qsearch ( 'cust_bill_pkg_tax_location', { %hash } ),
969 qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
974 =item recur_show_zero
978 sub recur_show_zero { shift->_X_show_zero('recur'); }
979 sub setup_show_zero { shift->_X_show_zero('setup'); }
982 my( $self, $what ) = @_;
984 return 0 unless $self->$what() == 0 && $self->pkgnum;
986 $self->cust_pkg->_X_show_zero($what);
989 =item credited [ BEFORE, AFTER, OPTIONS ]
991 Returns the sum of credits applied to this item. Arguments are the same as
992 owed_sql/paid_sql/credited_sql.
998 $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
1001 =item tax_locationnum
1003 Returns the L<FS::cust_location> number that this line item is in for tax
1004 purposes. For package sales, it's the package tax location; for fees,
1005 it's the customer's default service location.
1009 sub tax_locationnum {
1011 if ( $self->pkgnum ) { # normal sales
1012 return $self->cust_pkg->tax_locationnum;
1013 } elsif ( $self->feepart ) { # fees
1014 return $self->cust_bill->cust_main->ship_locationnum;
1022 if ( $self->pkgnum ) { # normal sales
1023 return $self->cust_pkg->tax_location;
1024 } elsif ( $self->feepart ) { # fees
1025 return $self->cust_bill->cust_main->ship_location;
1033 =head1 CLASS METHODS
1039 Returns an SQL expression for the total usage charges in details on
1045 '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
1046 FROM cust_bill_pkg_detail
1047 WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
1049 sub usage_sql { $usage_sql }
1051 # this makes owed_sql, etc. much more concise
1053 my ($class, $start, $end, %opt) = @_;
1054 my $setuprecur = $opt{setuprecur} || '';
1056 $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
1057 $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
1058 'cust_bill_pkg.setup + cust_bill_pkg.recur';
1060 if ($opt{no_usage} and $charged =~ /recur/) {
1061 $charged = "$charged - $usage_sql"
1068 =item owed_sql [ BEFORE, AFTER, OPTIONS ]
1070 Returns an SQL expression for the amount owed. BEFORE and AFTER specify
1071 a date window. OPTIONS may include 'no_usage' (excludes usage charges)
1072 and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
1078 '(' . $class->charged_sql(@_) .
1079 ' - ' . $class->paid_sql(@_) .
1080 ' - ' . $class->credited_sql(@_) . ')'
1083 =item paid_sql [ BEFORE, AFTER, OPTIONS ]
1085 Returns an SQL expression for the sum of payments applied to this item.
1090 my ($class, $start, $end, %opt) = @_;
1091 my $s = $start ? "AND cust_pay._date <= $start" : '';
1092 my $e = $end ? "AND cust_pay._date > $end" : '';
1093 my $setuprecur = $opt{setuprecur} || '';
1094 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1095 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1096 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1098 my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
1099 FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
1100 JOIN cust_pay USING (paynum)
1101 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1102 $s $e $setuprecur )";
1104 if ( $opt{no_usage} ) {
1105 # cap the amount paid at the sum of non-usage charges,
1106 # minus the amount credited against non-usage charges
1108 $class->charged_sql($start, $end, %opt) . ' - ' .
1109 $class->credited_sql($start, $end, %opt).')';
1118 my ($class, $start, $end, %opt) = @_;
1119 my $s = $start ? "AND cust_credit._date <= $start" : '';
1120 my $e = $end ? "AND cust_credit._date > $end" : '';
1121 my $setuprecur = $opt{setuprecur} || '';
1122 $setuprecur = 'setup' if $setuprecur =~ /^s/;
1123 $setuprecur = 'recur' if $setuprecur =~ /^r/;
1124 $setuprecur &&= "AND setuprecur = '$setuprecur'";
1126 my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
1127 FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
1128 JOIN cust_credit USING (crednum)
1129 WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
1130 $s $e $setuprecur )";
1132 if ( $opt{no_usage} ) {
1133 # cap the amount credited at the sum of non-usage charges
1134 "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
1142 sub upgrade_tax_location {
1143 # For taxes that were calculated/invoiced before cust_location refactoring
1144 # (May-June 2012), there are no cust_bill_pkg_tax_location records unless
1145 # they were calculated on a package-location basis. Create them here,
1146 # along with any necessary cust_location records and any tax exemption
1149 my ($class, %opt) = @_;
1150 # %opt may include 's' and 'e': start and end date ranges
1151 # and 'X': abort on any error, instead of just rolling back changes to
1154 my $oldAutoCommit = $FS::UID::AutoCommit;
1155 local $FS::UID::AutoCommit = 0;
1158 use FS::h_cust_main;
1159 use FS::h_cust_bill;
1161 use FS::h_cust_main_exemption;
1164 local $FS::cust_location::import = 1;
1166 my $conf = FS::Conf->new; # h_conf?
1167 return if $conf->exists('enable_taxproducts'); #don't touch this case
1168 my $use_ship = $conf->exists('tax-ship_address');
1169 my $use_pkgloc = $conf->exists('tax-pkg_address');
1171 my $date_where = '';
1173 $date_where .= " AND cust_bill._date >= $opt{s}";
1176 $date_where .= " AND cust_bill._date < $opt{e}";
1179 my $commit_each_invoice = 1 unless $opt{X};
1181 # if an invoice has either of these kinds of objects, then it doesn't
1182 # need to be upgraded...probably
1183 my $sub_has_tax_link = 'SELECT 1 FROM cust_bill_pkg_tax_location'.
1184 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1185 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum';
1186 my $sub_has_exempt = 'SELECT 1 FROM cust_tax_exempt_pkg'.
1187 ' JOIN cust_bill_pkg USING (billpkgnum)'.
1188 ' WHERE cust_bill_pkg.invnum = cust_bill.invnum'.
1189 ' AND exempt_monthly IS NULL';
1191 my %all_tax_names = (
1194 map { $_->taxname => 1 }
1195 qsearch('h_cust_main_county', { taxname => { op => '!=', value => '' }})
1198 my $search = FS::Cursor->new({
1199 table => 'cust_bill',
1201 extra_sql => "WHERE NOT EXISTS($sub_has_tax_link) ".
1202 "AND NOT EXISTS($sub_has_exempt) ".
1206 #print "Processing ".scalar(@invnums)." invoices...\n";
1210 while (my $cust_bill = $search->fetch) {
1211 my $invnum = $cust_bill->invnum;
1213 print STDERR "Invoice #$invnum\n";
1215 my %pkgpart_taxclass; # pkgpart => taxclass
1216 my %pkgpart_exempt_setup;
1217 my %pkgpart_exempt_recur;
1218 my $h_cust_bill = qsearchs('h_cust_bill',
1219 { invnum => $invnum,
1220 history_action => 'insert' });
1221 if (!$h_cust_bill) {
1222 warn "no insert record for invoice $invnum; skipped\n";
1223 #$date = $cust_bill->_date as a fallback?
1224 # We're trying to avoid using non-real dates (-d/-y invoice dates)
1225 # when looking up history records in other tables.
1228 my $custnum = $h_cust_bill->custnum;
1230 # Determine the address corresponding to this tax region.
1231 # It's either the bill or ship address of the customer as of the
1232 # invoice date-of-insertion. (Not necessarily the invoice date.)
1233 my $date = $h_cust_bill->history_date;
1234 local($FS::Record::qsearch_qualify_columns) = 0;
1235 my $h_cust_main = qsearchs('h_cust_main',
1236 { custnum => $custnum },
1237 FS::h_cust_main->sql_h_searchs($date)
1239 if (!$h_cust_main ) {
1240 warn "no historical address for cust#".$h_cust_bill->custnum."; skipped\n";
1242 # fallback to current $cust_main? sounds dangerous.
1245 # This is a historical customer record, so it has a historical address.
1246 # If there's no cust_location matching this custnum and address (there
1247 # probably isn't), create one.
1248 my %tax_loc; # keys are pkgnums, values are cust_location objects
1249 my $default_tax_loc;
1250 if ( $h_cust_main->bill_locationnum ) {
1251 # the location has already been upgraded
1253 $default_tax_loc = $h_cust_main->ship_location;
1255 $default_tax_loc = $h_cust_main->bill_location;
1258 $pre = 'ship_' if $use_ship and length($h_cust_main->get('ship_last'));
1259 my %hash = map { $_ => $h_cust_main->get($pre.$_) }
1260 FS::cust_main->location_fields;
1261 # not really needed for this, and often result in duplicate locations
1262 delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
1264 $hash{custnum} = $h_cust_main->custnum;
1265 $default_tax_loc = FS::cust_location->new(\%hash);
1266 my $error = $default_tax_loc->find_or_insert || $default_tax_loc->disable_if_unused;
1268 warn "couldn't create historical location record for cust#".
1269 $h_cust_main->custnum.": $error\n";
1274 $exempt_cust = 1 if $h_cust_main->tax;
1276 # classify line items
1278 my %nontax_items; # taxclass => array of cust_bill_pkg
1279 foreach my $item ($h_cust_bill->cust_bill_pkg) {
1280 my $pkgnum = $item->pkgnum;
1282 if ( $pkgnum == 0 ) {
1284 push @tax_items, $item;
1287 # (pkgparts really shouldn't change, right?)
1288 local($FS::Record::qsearch_qualify_columns) = 0;
1289 my $h_cust_pkg = qsearchs('h_cust_pkg', { pkgnum => $pkgnum },
1290 FS::h_cust_pkg->sql_h_searchs($date)
1292 if ( !$h_cust_pkg ) {
1293 warn "no historical package #".$item->pkgpart."; skipped\n";
1296 my $pkgpart = $h_cust_pkg->pkgpart;
1298 if ( $use_pkgloc and $h_cust_pkg->locationnum ) {
1299 # then this package already had a locationnum assigned, and that's
1300 # the one to use for tax calculation
1301 $tax_loc{$pkgnum} = FS::cust_location->by_key($h_cust_pkg->locationnum);
1303 # use the customer's bill or ship loc, which was inserted earlier
1304 $tax_loc{$pkgnum} = $default_tax_loc;
1307 if (!exists $pkgpart_taxclass{$pkgpart}) {
1308 local($FS::Record::qsearch_qualify_columns) = 0;
1309 my $h_part_pkg = qsearchs('h_part_pkg', { pkgpart => $pkgpart },
1310 FS::h_part_pkg->sql_h_searchs($date)
1312 if ( !$h_part_pkg ) {
1313 warn "no historical package def #$pkgpart; skipped\n";
1316 $pkgpart_taxclass{$pkgpart} = $h_part_pkg->taxclass || '';
1317 $pkgpart_exempt_setup{$pkgpart} = 1 if $h_part_pkg->setuptax;
1318 $pkgpart_exempt_recur{$pkgpart} = 1 if $h_part_pkg->recurtax;
1321 # mark any exemptions that apply
1322 if ( $pkgpart_exempt_setup{$pkgpart} ) {
1323 $item->set('exempt_setup' => 1);
1326 if ( $pkgpart_exempt_recur{$pkgpart} ) {
1327 $item->set('exempt_recur' => 1);
1330 my $taxclass = $pkgpart_taxclass{ $pkgpart };
1332 $nontax_items{$taxclass} ||= [];
1333 push @{ $nontax_items{$taxclass} }, $item;
1337 printf("%d tax items: \$%.2f\n", scalar(@tax_items), map {$_->setup} @tax_items)
1340 # Get any per-customer taxname exemptions that were in effect.
1341 my %exempt_cust_taxname;
1342 foreach (keys %all_tax_names) {
1343 local($FS::Record::qsearch_qualify_columns) = 0;
1344 my $h_exemption = qsearchs('h_cust_main_exemption', {
1345 'custnum' => $custnum,
1348 FS::h_cust_main_exemption->sql_h_searchs($date, $date)
1351 $exempt_cust_taxname{ $_ } = 1;
1355 # Use a variation on the procedure in
1356 # FS::cust_main::Billing::_handle_taxes to identify taxes that apply
1358 my @loc_keys = qw( district city county state country );
1359 my %taxdef_by_name; # by name, and then by taxclass
1360 my %est_tax; # by name, and then by taxclass
1361 my %taxable_items; # by taxnum, and then an array
1363 foreach my $taxclass (keys %nontax_items) {
1364 foreach my $orig_item (@{ $nontax_items{$taxclass} }) {
1365 my $my_tax_loc = $tax_loc{ $orig_item->pkgnum };
1366 my %myhash = map { $_ => $my_tax_loc->get($pre.$_) } @loc_keys;
1367 my @elim = qw( district city county state );
1368 my @taxdefs; # because there may be several with different taxnames
1370 $myhash{taxclass} = $taxclass;
1371 @taxdefs = qsearch('cust_main_county', \%myhash);
1373 $myhash{taxclass} = '';
1374 @taxdefs = qsearch('cust_main_county', \%myhash);
1376 $myhash{ shift @elim } = '';
1377 } while scalar(@elim) and !@taxdefs;
1379 foreach my $taxdef (@taxdefs) {
1380 next if $taxdef->tax == 0;
1381 $taxdef_by_name{$taxdef->taxname}{$taxdef->taxclass} = $taxdef;
1383 $taxable_items{$taxdef->taxnum} ||= [];
1384 # clone the item so that taxdef-dependent changes don't
1385 # change it for other taxdefs
1386 my $item = FS::cust_bill_pkg->new({ $orig_item->hash });
1388 # these flags are already set if the part_pkg declares itself exempt
1389 $item->set('exempt_setup' => 1) if $taxdef->setuptax;
1390 $item->set('exempt_recur' => 1) if $taxdef->recurtax;
1393 my $taxable = $item->setup + $item->recur;
1395 # h_cust_credit_bill_pkg?
1396 # NO. Because if these exemptions HAD been created at the time of
1397 # billing, and then a credit applied later, the exemption would
1398 # have been adjusted by the amount of the credit. So we adjust
1399 # the taxable amount before creating the exemption.
1400 # But don't deduct the credit from taxable, because the tax was
1401 # calculated before the credit was applied.
1402 foreach my $f (qw(setup recur)) {
1403 my $credited = FS::Record->scalar_sql(
1404 "SELECT SUM(amount) FROM cust_credit_bill_pkg ".
1405 "WHERE billpkgnum = ? AND setuprecur = ?",
1409 $item->set($f, $item->get($f) - $credited) if $credited;
1411 my $existing_exempt = FS::Record->scalar_sql(
1412 "SELECT SUM(amount) FROM cust_tax_exempt_pkg WHERE ".
1413 "billpkgnum = ? AND taxnum = ?",
1414 $item->billpkgnum, $taxdef->taxnum
1416 $taxable -= $existing_exempt;
1418 if ( $taxable and $exempt_cust ) {
1419 push @new_exempt, { exempt_cust => 'Y', amount => $taxable };
1422 if ( $taxable and $exempt_cust_taxname{$taxdef->taxname} ){
1423 push @new_exempt, { exempt_cust_taxname => 'Y', amount => $taxable };
1426 if ( $taxable and $item->exempt_setup ) {
1427 push @new_exempt, { exempt_setup => 'Y', amount => $item->setup };
1428 $taxable -= $item->setup;
1430 if ( $taxable and $item->exempt_recur ) {
1431 push @new_exempt, { exempt_recur => 'Y', amount => $item->recur };
1432 $taxable -= $item->recur;
1435 $item->set('taxable' => $taxable);
1436 push @{ $taxable_items{$taxdef->taxnum} }, $item
1439 # estimate the amount of tax (this is necessary because different
1440 # taxdefs with the same taxname may have different tax rates)
1441 # and sum that for each taxname/taxclass combination
1443 $est_tax{$taxdef->taxname} ||= {};
1444 $est_tax{$taxdef->taxname}{$taxdef->taxclass} ||= 0;
1445 $est_tax{$taxdef->taxname}{$taxdef->taxclass} +=
1446 $taxable * $taxdef->tax;
1448 foreach (@new_exempt) {
1449 next if $_->{amount} == 0;
1450 my $cust_tax_exempt_pkg = FS::cust_tax_exempt_pkg->new({
1452 billpkgnum => $item->billpkgnum,
1453 taxnum => $taxdef->taxnum,
1455 my $error = $cust_tax_exempt_pkg->insert;
1457 my $pkgnum = $item->pkgnum;
1458 warn "error creating tax exemption for inv$invnum pkg$pkgnum:".
1462 } #foreach @new_exempt
1465 } #foreach $taxclass
1467 # Now go through the billed taxes and match them up with the line items.
1468 TAX_ITEM: foreach my $tax_item ( @tax_items )
1470 my $taxname = $tax_item->itemdesc;
1471 $taxname = '' if $taxname eq 'Tax';
1473 if ( !exists( $taxdef_by_name{$taxname} ) ) {
1474 # then we didn't find any applicable taxes with this name
1475 warn "no definition found for tax item '$taxname', custnum $custnum\n";
1476 # possibly all of these should be "next TAX_ITEM", but whole invoices
1477 # are transaction protected and we can go back and retry them.
1480 # classname => cust_main_county
1481 my %taxdef_by_class = %{ $taxdef_by_name{$taxname} };
1483 # Divide the tax item among taxclasses, if necessary
1484 # classname => estimated tax amount
1485 my $this_est_tax = $est_tax{$taxname};
1486 if (!defined $this_est_tax) {
1487 warn "no taxable sales found for inv#$invnum, tax item '$taxname'.\n";
1490 my $est_total = sum(values %$this_est_tax);
1491 if ( $est_total == 0 ) {
1493 warn "estimated tax on invoice #$invnum is zero.\n";
1497 my $real_tax = $tax_item->setup;
1498 printf ("Distributing \$%.2f tax:\n", $real_tax);
1499 my $cents_remaining = $real_tax * 100; # for rounding error
1500 my @tax_links; # partial CBPTL hashrefs
1501 foreach my $taxclass (keys %taxdef_by_class) {
1502 my $taxdef = $taxdef_by_class{$taxclass};
1503 # these items already have "taxable" set to their charge amount
1504 # after applying any credits or exemptions
1505 my @items = @{ $taxable_items{$taxdef->taxnum} };
1506 my $subtotal = sum(map {$_->get('taxable')} @items);
1507 printf("\t$taxclass: %.2f\n", $this_est_tax->{$taxclass}/$est_total);
1509 foreach my $nontax (@items) {
1510 my $my_tax_loc = $tax_loc{ $nontax->pkgnum };
1511 my $part = int($real_tax
1513 * ($this_est_tax->{$taxclass}/$est_total)
1515 * ($nontax->get('taxable'))/$subtotal
1519 $cents_remaining -= $part;
1521 taxnum => $taxdef->taxnum,
1522 pkgnum => $nontax->pkgnum,
1523 locationnum => $my_tax_loc->locationnum,
1524 billpkgnum => $nontax->billpkgnum,
1528 } #foreach $taxclass
1529 # Distribute any leftover tax round-robin style, one cent at a time.
1531 my $nlinks = scalar(@tax_links);
1533 # ensure that it really is an integer
1534 $cents_remaining = sprintf('%.0f', $cents_remaining);
1535 while ($cents_remaining > 0) {
1536 $tax_links[$i % $nlinks]->{cents} += 1;
1541 warn "Can't create tax links--no taxable items found.\n";
1545 # Gather credit/payment applications so that we can link them
1548 qsearch( 'cust_credit_bill_pkg',
1549 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1551 qsearch( 'cust_bill_pay_pkg',
1552 { billpkgnum => $tax_item->billpkgnum, billpkgtaxlocationnum => '' }
1556 # grab the first one
1557 my $this_unlinked = shift @unlinked;
1558 my $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1560 # Create tax links (yay!)
1561 printf("Creating %d tax links.\n",scalar(@tax_links));
1562 foreach (@tax_links) {
1563 my $link = FS::cust_bill_pkg_tax_location->new({
1564 billpkgnum => $tax_item->billpkgnum,
1565 taxtype => 'FS::cust_main_county',
1566 locationnum => $_->{locationnum},
1567 taxnum => $_->{taxnum},
1568 pkgnum => $_->{pkgnum},
1569 amount => sprintf('%.2f', $_->{cents} / 100),
1570 taxable_billpkgnum => $_->{billpkgnum},
1572 my $error = $link->insert;
1574 warn "Can't create tax link for inv#$invnum: $error\n";
1578 my $link_cents = $_->{cents};
1579 # update/create subitem links
1581 # If $this_unlinked is undef, then we've allocated all of the
1582 # credit/payment applications to the tax item. If $link_cents is 0,
1583 # then we've applied credits/payments to all of this package fraction,
1584 # so go on to the next.
1585 while ($this_unlinked and $link_cents) {
1586 # apply as much as possible of $link_amount to this credit/payment
1588 my $apply_cents = min($link_cents, $unlinked_cents);
1589 $link_cents -= $apply_cents;
1590 $unlinked_cents -= $apply_cents;
1591 # $link_cents or $unlinked_cents or both are now zero
1592 $this_unlinked->set('amount' => sprintf('%.2f',$apply_cents/100));
1593 $this_unlinked->set('billpkgtaxlocationnum' => $link->billpkgtaxlocationnum);
1594 my $pkey = $this_unlinked->primary_key; #creditbillpkgnum or billpaypkgnum
1595 if ( $this_unlinked->$pkey ) {
1596 # then it's an existing link--replace it
1597 $error = $this_unlinked->replace;
1599 $this_unlinked->insert;
1601 # what do we do with errors at this stage?
1603 warn "Error creating tax application link: $error\n";
1604 next INVOICE; # for lack of a better idea
1607 if ( $unlinked_cents == 0 ) {
1608 # then we've allocated all of this payment/credit application,
1609 # so grab the next one
1610 $this_unlinked = shift @unlinked;
1611 $unlinked_cents = int($this_unlinked->amount * 100) if $this_unlinked;
1612 } elsif ( $link_cents == 0 ) {
1613 # then we've covered all of this package tax fraction, so split
1614 # off a new application from this one
1615 $this_unlinked = $this_unlinked->new({
1616 $this_unlinked->hash,
1619 # $unlinked_cents is still what it is
1622 } #while $this_unlinked and $link_cents
1623 } #foreach (@tax_links)
1624 } #foreach $tax_item
1626 $dbh->commit if $commit_each_invoice and $oldAutoCommit;
1632 $dbh->rollback if $oldAutoCommit;
1633 die "Upgrade halted.\n" unless $commit_each_invoice;
1637 $dbh->commit if $oldAutoCommit and !$commit_each_invoice;
1642 # Create a queue job to run upgrade_tax_location from January 1, 2012 to
1646 use Date::Parse 'str2time';
1649 my $upgrade = 'tax_location_2012';
1650 return if FS::upgrade_journal->is_done($upgrade);
1651 my $job = FS::queue->new({
1652 'job' => 'FS::cust_bill_pkg::upgrade_tax_location'
1654 # call it kind of like a class method, not that it matters much
1655 $job->insert($class, 's' => str2time('2012-01-01'));
1656 # if there's a customer location upgrade queued also, wait for it to
1658 my $location_job = qsearchs('queue', {
1659 job => 'FS::cust_main::Location::process_upgrade_location'
1661 if ( $location_job ) {
1662 $job->depend_insert($location_job->jobnum);
1664 # Then mark the upgrade as done, so that we don't queue the job twice
1665 # and somehow run two of them concurrently.
1666 FS::upgrade_journal->set_done($upgrade);
1667 # This upgrade now does the job of assigning taxable_billpkgnums to
1668 # cust_bill_pkg_tax_location, so set that task done also.
1669 FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
1676 setup and recur shouldn't be separate fields. There should be one "amount"
1677 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1679 A line item with both should really be two separate records (preserving
1680 sdate and edate for setup fees for recurring packages - that information may
1681 be valuable later). Invoice generation (cust_main::bill), invoice printing
1682 (cust_bill), tax reports (report_tax.cgi) and line item reports
1683 (cust_bill_pkg.cgi) would need to be updated.
1685 owed_setup and owed_recur could then be repaced by just owed, and
1686 cust_bill::open_cust_bill_pkg and
1687 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1689 The upgrade procedure is pretty sketchy.
1693 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1694 from the base documentation.