we don't actually need the results ordered, and i'm sure it doesn't help the planner...
[freeside.git] / FS / FS / Cron / bill.pm
1 package FS::Cron::bill;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK );
5 use Exporter;
6 use Date::Parse;
7 use DBI 1.33; #The "clone" method was added in DBI 1.33. 
8 use FS::UID qw(dbh);
9 use FS::Record qw(qsearchs);
10 use FS::cust_main;
11
12 @ISA = qw( Exporter );
13 @EXPORT_OK = qw ( bill );
14
15 sub bill {
16
17   my %opt = @_;
18
19   my $debug = 0;
20   $debug = 1 if $opt{'v'};
21   $debug = $opt{'l'} if $opt{'l'};
22  
23   $FS::cust_main::DEBUG = $debug;
24   #$FS::cust_event::DEBUG = $opt{'l'} if $opt{'l'};
25
26   my @search = ();
27
28   push @search, "( cust_main.archived != 'Y' OR archived IS NULL )"; #disable?
29
30   push @search, "cust_main.payby    = '". $opt{'p'}. "'"
31     if $opt{'p'};
32   push @search, "cust_main.agentnum =  ". $opt{'a'}
33     if $opt{'a'};
34
35   if ( @ARGV ) {
36     push @search, "( ".
37       join(' OR ', map "cust_main.custnum = $_", @ARGV ).
38     " )";
39   }
40
41   ###
42   # generate where_pkg / where_bill_event search clause (1.7-style)
43   ###
44
45   #we're at now now (and later).
46   my($time)= $opt{'d'} ? str2time($opt{'d'}) : $^T;
47   $time += $opt{'y'} * 86400 if $opt{'y'};
48
49   my $invoice_time = $opt{'n'} ? $^T : $time;
50
51   # select * from cust_main where
52   my $where_pkg = <<"END";
53     0 < ( select count(*) from cust_pkg
54             where cust_main.custnum = cust_pkg.custnum
55               and ( cancel is null or cancel = 0 )
56               and (    setup is null or setup =  0
57                     or bill  is null or bill  <= $time 
58                     or ( expire is not null and expire <= $^T )
59                     or ( adjourn is not null and adjourn <= $^T )
60                   )
61         )
62 END
63   
64   # or
65   my $where_bill_event = <<"END";
66     0 < ( select count(*) from cust_bill
67             where cust_main.custnum = cust_bill.custnum
68               and 0 < charged
69                       - coalesce(
70                                   ( select sum(amount) from cust_bill_pay
71                                       where cust_bill.invnum = cust_bill_pay.invnum )
72                                   ,0
73                                 )
74                       - coalesce(
75                                   ( select sum(amount) from cust_credit_bill
76                                       where cust_bill.invnum = cust_credit_bill.invnum )
77                                   ,0
78                                 )
79               and 0 < ( select count(*) from part_bill_event
80                           where payby = cust_main.payby
81                             and ( disabled is null or disabled = '' )
82                             and seconds <= $time - cust_bill._date
83                             and 0 = ( select count(*) from cust_bill_event
84                                        where cust_bill.invnum = cust_bill_event.invnum
85                                          and part_bill_event.eventpart = cust_bill_event.eventpart
86                                          and status = 'done'
87                                     )
88   
89                       )
90         )
91 END
92   
93   push @search, "( $where_pkg OR $where_bill_event )";
94
95   ###
96   # get a list of custnums
97   ###
98
99   warn "searching for customers:\n". join("\n", @search). "\n"
100     if $opt{'v'} || $opt{'l'};
101
102   my $cursor_dbh = dbh->clone;
103
104   $cursor_dbh->do(
105     "DECLARE cron_bill_cursor CURSOR FOR ".
106     "  SELECT custnum FROM cust_main WHERE ". join(' AND ', @search)
107   ) or die $cursor_dbh->errstr;
108   
109   while ( 1 ) {
110   
111     my $sth = $cursor_dbh->prepare('FETCH 1000 FROM cron_bill_cursor'); #mysql?
112   
113     $sth->execute or die $sth->errstr;
114
115     my @custnums = map { $_->[0] } @{ $sth->fetchall_arrayref };
116
117     last unless scalar(@custnums);
118
119     ###
120     # for each custnum, queue or make one customer object and bill
121     # (one at a time, to reduce memory footprint with large #s of customers)
122     ###
123     
124     foreach my $custnum ( @custnums ) {
125     
126       my %args = (
127           'time'         => $time,
128           'invoice_time' => $invoice_time,
129           'actual_time'  => $^T, #when freeside-bill was started
130                                  #(not, when using -m, freeside-queued)
131           'resetup'      => ( $opt{'s'} ? $opt{'s'} : 0 ),
132       );
133
134       if ( $opt{'m'} ) {
135
136         #add job to queue that calls bill_and_collect with options
137         my $queue = new FS::queue {
138           'job'      => 'FS::cust_main::queued_bill',
139           'priority' => 99, #don't get in the way of provisioning jobs
140         };
141         my $error = $queue->insert( 'custnum'=>$custnum, %args );
142
143       } else {
144
145         my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } );
146         $cust_main->bill_and_collect( %args, 'debug' => $debug );
147
148       }
149
150     }
151
152   }
153
154   $cursor_dbh->commit or die $cursor_dbh->errstr;
155
156 }
157
158 1;