RT# 81961 Repair broken links in POD documentation
[freeside.git] / FS / FS / cust_credit_bill_pkg.pm
1 package FS::cust_credit_bill_pkg;
2
3 use strict;
4 use vars qw( @ISA );
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::cust_main_Mixin;
7 use FS::cust_credit_bill;
8 use FS::cust_bill_pkg;
9 use FS::cust_bill_pkg_tax_location;
10 use FS::cust_bill_pkg_tax_rate_location;
11 use FS::cust_tax_exempt_pkg;
12
13 @ISA = qw( FS::cust_main_Mixin FS::Record );
14
15 =head1 NAME
16
17 FS::cust_credit_bill_pkg - Object methods for cust_credit_bill_pkg records
18
19 =head1 SYNOPSIS
20
21   use FS::cust_credit_bill_pkg;
22
23   $record = new FS::cust_credit_bill_pkg \%hash;
24   $record = new FS::cust_credit_bill_pkg { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::cust_credit_bill_pkg object represents application of a credit (see 
37 L<FS::cust_credit_bill>) to a specific line item within an invoice
38 (see L<FS::cust_bill_pkg>).  FS::cust_credit_bill_pkg inherits from FS::Record.
39 The following fields are currently supported:
40
41 =over 4
42
43 =item creditbillpkgnum -  primary key
44
45 =item creditbillnum - Credit application to the overall invoice (see L<FS::cust_credit_bill>)
46
47 =item billpkgnum - Line item to which credit is applied (see L<FS::cust_bill_pkg>)
48
49 =item amount - Amount of the credit applied to this line item.
50
51 =item setuprecur - 'setup' or 'recur', designates whether the payment was applied to the setup or recurring portion of the line item.
52
53 =item sdate - starting date of recurring fee
54
55 =item edate - ending date of recurring fee
56
57 =back
58
59 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
60 see L<Time::Local> and L<Date::Parse> for conversion functions.
61
62 =head1 METHODS
63
64 =over 4
65
66 =item new HASHREF
67
68 Creates a new example.  To add the example to the database, see L<"insert">.
69
70 Note that this stores the hash reference, not a distinct copy of the hash it
71 points to.  You can ask the object for a copy with the I<hash> method.
72
73 =cut
74
75 # the new method can be inherited from FS::Record, if a table method is defined
76
77 sub table { 'cust_credit_bill_pkg'; }
78
79 =item insert
80
81 Adds this record to the database.  If there is an error, returns the error,
82 otherwise returns false.
83
84 =cut
85
86 sub insert {
87   my $self = shift;
88
89   local $SIG{HUP} = 'IGNORE';
90   local $SIG{INT} = 'IGNORE';
91   local $SIG{QUIT} = 'IGNORE';
92   local $SIG{TERM} = 'IGNORE';
93   local $SIG{TSTP} = 'IGNORE';
94   local $SIG{PIPE} = 'IGNORE';
95
96   my $oldAutoCommit = $FS::UID::AutoCommit;
97   local $FS::UID::AutoCommit = 0;
98   my $dbh = dbh;
99
100   my $error = $self->SUPER::insert;
101   if ( $error ) {
102     $dbh->rollback if $oldAutoCommit;
103     return $error;
104   }
105
106   my $cust_bill_pkg = $self->cust_bill_pkg;
107   #'payable' is the amount charged (either setup or recur)
108   # minus any credit applications, including this one
109   my $payable = $cust_bill_pkg->payable($self->setuprecur);
110   my $part_pkg = $cust_bill_pkg->part_pkg;
111   my $freq = $cust_bill_pkg->freq;
112   unless ($freq) {
113     $freq = $part_pkg ? ($part_pkg->freq || 1) : 1;#fallback.. assumes unchanged
114   }
115   my $taxable_per_month = sprintf("%.2f", $payable / $freq );
116   my $credit_per_month = sprintf("%.2f", $self->amount / $freq ); #pennies?
117
118   if ($taxable_per_month >= 0) {  #panic if its subzero?
119     my $groupby = join(',',
120       qw(taxnum taxtype year month exempt_monthly exempt_cust 
121          exempt_cust_taxname exempt_setup exempt_recur));
122     my $sum = 'SUM(amount)';
123     my @exemptions = qsearch(
124       {
125         'select'    => "$groupby, $sum AS amount",
126         'table'     => 'cust_tax_exempt_pkg',
127         'hashref'   => { billpkgnum => $self->billpkgnum },
128         'extra_sql' => "GROUP BY $groupby HAVING $sum > 0",
129       }
130     ); 
131     # each $exemption is now the sum of all monthly exemptions applied to 
132     # this line item for a particular taxnum and month.
133     foreach my $exemption ( @exemptions ) {
134       my $amount = 0;
135       if ( $exemption->exempt_monthly ) {
136         # finite exemptions
137         # $taxable_per_month is AFTER inserting the credit application, so 
138         # if it's still larger than the exemption, we don't need to adjust
139         next if $taxable_per_month >= $exemption->amount;
140         # the amount of 'excess' exemption already in place (above the 
141         # remaining charged amount).  We'll de-exempt that much, or the 
142         # amount of the new credit, whichever is smaller.
143         $amount = $exemption->amount - $taxable_per_month;
144         # $amount is the amount of 'excess' exemption already existing 
145         # (above the remaining taxable charge amount).  We'll "de-exempt"
146         # that much, or the amount of the new credit, whichever is smaller.
147         if ($amount > $credit_per_month) {
148                "cust_bill_pkg ". $self->billpkgnum. "  Reducing.\n";
149           $amount = $credit_per_month;
150         }
151       } elsif ( $exemption->exempt_setup or $exemption->exempt_recur ) {
152         # package defined exemptions: may be setup only, recur only, or both
153         my $method = 'exempt_'.$self->setuprecur;
154         if ( $exemption->$method ) {
155           # then it's exempt from the portion of the charge that this 
156           # credit is being applied to
157           $amount = $self->amount;
158         }
159       } else {
160         # other types of exemptions: always equal to the amount of
161         # the charge
162         $amount = $self->amount;
163       }
164       next if $amount == 0;
165
166       # create a negative exemption
167       my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg {
168          $exemption->hash, # for exempt_ flags, taxnum, month/year
169         'billpkgnum'       => $self->billpkgnum,
170         'creditbillpkgnum' => $self->creditbillpkgnum,
171         'amount'           => sprintf('%.2f', 0-$amount),
172       };
173
174       if ( $cust_tax_exempt_pkg->cust_main_county ) {
175
176         my $error = $cust_tax_exempt_pkg->insert;
177         if ( $error ) {
178           $dbh->rollback if $oldAutoCommit;
179           return "error inserting cust_tax_exempt_pkg: $error";
180         }
181
182       }
183
184     } #foreach $exemption
185   }
186
187   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
188  '';
189
190 }
191
192 #helper functions for above
193 sub _is_taxable {
194   my $self = shift;
195   my $part_pkg = $self->cust_bill_pkg->part_pkg;
196
197   return 0 unless $part_pkg; #XXX fails for tax on tax
198
199   my $method = $self->setuprecur. 'tax';
200   return 0 if $part_pkg->$method =~ /^Y$/i;
201
202   if ($self->billpkgtaxlocationnum) {
203     my $location_object = $self->cust_bill_pkg_tax_Xlocation;
204     my $tax_object = $location_object->cust_main_county;
205     return 0 if $tax_object && $self->tax_object->$method =~ /^Y$/i;
206   } #elsif ($self->billpkgtaxratelocationnum) { ... }
207
208   1;
209 }
210
211 =item delete
212
213 Delete this record from the database.
214
215 =cut
216
217 sub delete {
218   my $self = shift;
219
220   local $SIG{HUP} = 'IGNORE';
221   local $SIG{INT} = 'IGNORE';
222   local $SIG{QUIT} = 'IGNORE';
223   local $SIG{TERM} = 'IGNORE';
224   local $SIG{TSTP} = 'IGNORE';
225   local $SIG{PIPE} = 'IGNORE';
226
227   my $oldAutoCommit = $FS::UID::AutoCommit;
228   local $FS::UID::AutoCommit = 0;
229   my $dbh = dbh;
230
231   my @negative_exemptions = qsearch('cust_tax_exempt_pkg', {
232       'creditbillpkgnum' => $self->creditbillpkgnum
233   });
234
235   # de-anti-exempt those negative exemptions
236   my $error;
237   foreach (@negative_exemptions) {
238     $error = $_->delete;
239     if ( $error ) {
240       $dbh->rollback if $oldAutoCommit;
241       return $error;
242     }
243   }
244
245   $error = $self->SUPER::delete(@_);
246   if ( $error ) {
247     $dbh->rollback if $oldAutoCommit;
248     return $error;
249   }
250
251   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
252
253   '';
254
255 }
256
257 =item replace OLD_RECORD
258
259 Replaces the OLD_RECORD with this one in the database.  If there is an error,
260 returns the error, otherwise returns false.
261
262 =cut
263
264 # the replace method can be inherited from FS::Record
265
266 =item check
267
268 Checks all fields to make sure this is a valid credit applicaiton.  If there is
269 an error, returns the error, otherwise returns false.  Called by the insert
270 and replace methods.
271
272 =cut
273
274 # the check method should currently be supplied - FS::Record contains some
275 # data checking routines
276
277 sub check {
278   my $self = shift;
279
280   my $error = 
281     $self->ut_numbern('creditbillpkgnum')
282     || $self->ut_foreign_key('creditbillnum', 'cust_credit_bill', 'creditbillnum')
283     || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum' )
284     || $self->ut_foreign_keyn('billpkgtaxlocationnum',
285                               'cust_bill_pkg_tax_location',
286                               'billpkgtaxlocationnum')
287     || $self->ut_foreign_keyn('billpkgtaxratelocationnum',
288                               'cust_bill_pkg_tax_rate_location',
289                               'billpkgtaxratelocationnum')
290     || $self->ut_money('amount')
291     || $self->ut_enum('setuprecur', [ 'setup', 'recur' ] )
292     || $self->ut_numbern('sdate')
293     || $self->ut_numbern('edate')
294   ;
295   return $error if $error;
296
297   $self->SUPER::check;
298 }
299
300 sub cust_credit_bill {
301   my $self = shift;
302   qsearchs('cust_credit_bill', { 'creditbillnum' => $self->creditbillnum } );
303 }
304
305 sub cust_bill_pkg {
306   my $self = shift;
307   qsearchs('cust_bill_pkg', { 'billpkgnum' => $self->billpkgnum } );
308 }
309
310 sub cust_bill_pkg_tax_Xlocation {
311   my $self = shift;
312   if ($self->billpkgtaxlocationnum) {
313     return qsearchs(
314       'cust_bill_pkg_tax_location',
315       { 'billpkgtaxlocationnum' => $self->billpkgtaxlocationnum },
316     );
317  
318   } elsif ($self->billpkgtaxratelocationnum) {
319     return qsearchs(
320       'cust_bill_pkg_tax_rate_location',
321       { 'billpkgtaxratelocationnum' => $self->billpkgtaxratelocationnum },
322     );
323   } else {
324     return undef;
325   }
326 }
327
328 =back
329
330 =head1 BUGS
331
332 B<setuprecur> field is a kludge to compensate for cust_bill_pkg having separate
333 setup and recur fields.  It should be removed once that's fixed.
334
335 B<insert> method used to assume that the frequency of the package associated
336 with the associated line item remained unchanged during the lifetime of the
337 system.  That is still used as a fallback.  It may get the tax exemption
338 adjustments wrong if package definitions change frequency.  The presense of
339 delete methods in FS::cust_main_county and FS::tax_rate makes crediting of
340 old "texas tax" unreliable in the presense of changing taxes.  Explicit tax
341 credit requests?  Carry 'taxable' onto line items?
342
343 =head1 SEE ALSO
344
345 L<FS::Record>, schema.html from the base documentation.
346
347 =cut
348
349 1;
350