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