convert prospects to customers via quotations, RT#20688
[freeside.git] / FS / FS / quotation.pm
1 package FS::quotation;
2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
3
4 use strict;
5 use Tie::RefHash;
6 use FS::UID qw( dbh );
7 use FS::Record qw( qsearch qsearchs );
8 use FS::CurrentUser;
9 use FS::cust_main;
10 use FS::cust_pkg;
11 use FS::prospect_main;
12 use FS::quotation_pkg;
13
14 =head1 NAME
15
16 FS::quotation - Object methods for quotation records
17
18 =head1 SYNOPSIS
19
20   use FS::quotation;
21
22   $record = new FS::quotation \%hash;
23   $record = new FS::quotation { 'column' => 'value' };
24
25   $error = $record->insert;
26
27   $error = $new_record->replace($old_record);
28
29   $error = $record->delete;
30
31   $error = $record->check;
32
33 =head1 DESCRIPTION
34
35 An FS::quotation object represents a quotation.  FS::quotation inherits from
36 FS::Record.  The following fields are currently supported:
37
38 =over 4
39
40 =item quotationnum
41
42 primary key
43
44 =item prospectnum
45
46 prospectnum
47
48 =item custnum
49
50 custnum
51
52 =item _date
53
54 _date
55
56 =item disabled
57
58 disabled
59
60 =item usernum
61
62 usernum
63
64
65 =back
66
67 =head1 METHODS
68
69 =over 4
70
71 =item new HASHREF
72
73 Creates a new quotation.  To add the quotation to the database, see L<"insert">.
74
75 Note that this stores the hash reference, not a distinct copy of the hash it
76 points to.  You can ask the object for a copy with the I<hash> method.
77
78 =cut
79
80 sub table { 'quotation'; }
81 sub notice_name { 'Quotation'; }
82 sub template_conf { 'quotation_'; }
83
84 =item insert
85
86 Adds this record to the database.  If there is an error, returns the error,
87 otherwise returns false.
88
89 =item delete
90
91 Delete this record from the database.
92
93 =item replace OLD_RECORD
94
95 Replaces the OLD_RECORD with this one in the database.  If there is an error,
96 returns the error, otherwise returns false.
97
98 =item check
99
100 Checks all fields to make sure this is a valid quotation.  If there is
101 an error, returns the error, otherwise returns false.  Called by the insert
102 and replace methods.
103
104 =cut
105
106 sub check {
107   my $self = shift;
108
109   my $error = 
110     $self->ut_numbern('quotationnum')
111     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
112     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
113     || $self->ut_numbern('_date')
114     || $self->ut_enum('disabled', [ '', 'Y' ])
115     || $self->ut_numbern('usernum')
116   ;
117   return $error if $error;
118
119   $self->_date(time) unless $self->_date;
120
121   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
122
123   $self->SUPER::check;
124 }
125
126 =item prospect_main
127
128 =cut
129
130 sub prospect_main {
131   my $self = shift;
132   qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
133 }
134
135 =item cust_main
136
137 =cut
138
139 sub cust_main {
140   my $self = shift;
141   qsearchs('cust_main', { 'custnum' => $self->custnum } );
142 }
143
144 =item cust_bill_pkg
145
146 =cut
147
148 sub cust_bill_pkg { #actually quotation_pkg objects
149   my $self = shift;
150   qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
151 }
152
153 =item total_setup
154
155 =cut
156
157 sub total_setup {
158   my $self = shift;
159   $self->_total('setup');
160 }
161
162 =item total_recur [ FREQ ]
163
164 =cut
165
166 sub total_recur {
167   my $self = shift;
168 #=item total_recur [ FREQ ]
169   #my $freq = @_ ? shift : '';
170   $self->_total('recur');
171 }
172
173 sub _total {
174   my( $self, $method ) = @_;
175
176   my $total = 0;
177   $total += $_->$method() for $self->cust_bill_pkg;
178   sprintf('%.2f', $total);
179
180 }
181
182 #prevent things from falsely showing up as taxes, at least until we support
183 # quoting tax amounts..
184 sub _items_tax {
185   return ();
186 }
187 sub _items_nontax {
188   shift->cust_bill_pkg;
189 }
190
191 sub _items_total {
192   my( $self, $total_items ) = @_;
193
194   if ( $self->total_setup > 0 ) {
195     push @$total_items, {
196       'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
197       'total_amount' => $self->total_setup,
198     };
199   }
200
201   #could/should add up the different recurring frequencies on lines of their own
202   # but this will cover the 95% cases for now
203   if ( $self->total_recur > 0 ) {
204     push @$total_items, {
205       'total_item'   => $self->mt('Total Recurring'),
206       'total_amount' => $self->total_recur,
207     };
208   }
209
210 }
211
212 =item enable_previous
213
214 =cut
215
216 sub enable_previous { 0 }
217
218 =item convert_cust_main
219
220 If this quotation already belongs to a customer, then returns that customer, as
221 an FS::cust_main object.
222
223 Otherwise, creates a new customer (FS::cust_main object and record, and
224 associated) based on this quotation's prospect, then orders this quotation's
225 packages as real packages for the customer.
226
227 If there is an error, returns an error message, otherwise, returns the
228 newly-created FS::cust_main object.
229
230 =cut
231
232 sub convert_cust_main {
233   my $self = shift;
234
235   my $cust_main = $self->cust_main;
236   return $cust_main if $cust_main; #already converted, don't again
237
238   my $oldAutoCommit = $FS::UID::AutoCommit;
239   local $FS::UID::AutoCommit = 0;
240   my $dbh = dbh;
241
242   $cust_main = $self->prospect_main->convert_cust_main;
243   unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
244     $dbh->rollback if $oldAutoCommit;
245     return $cust_main;
246   }
247
248   $self->prospectnum('');
249   $self->custnum( $cust_main->custnum );
250   my $error = $self->replace || $self->order;
251   if ( $error ) {
252     $dbh->rollback if $oldAutoCommit;
253     return $error;
254   }
255
256   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
257
258   $cust_main;
259
260 }
261
262 =item order
263
264 This method is for use with quotations which are already associated with a customer.
265
266 Orders this quotation's packages as real packages for the customer.
267
268 If there is an error, returns an error message, otherwise returns false.
269
270 =cut
271
272 sub order {
273   my $self = shift;
274
275   tie my %cust_pkg, 'Tie::RefHash',
276     map { FS::cust_pkg->new({ pkgpart  => $_->pkgpart,
277                               quantity => $_->quantity,
278                            })
279             => [] #services
280         }
281       $self->quotation_pkg ;
282
283   $self->cust_main->order_pkgs( \%cust_pkg );
284
285 }
286
287 =back
288
289 =head1 CLASS METHODS
290
291 =over 4
292
293
294 =item search_sql_where HASHREF
295
296 Class method which returns an SQL WHERE fragment to search for parameters
297 specified in HASHREF.  Valid parameters are
298
299 =over 4
300
301 =item _date
302
303 List reference of start date, end date, as UNIX timestamps.
304
305 =item invnum_min
306
307 =item invnum_max
308
309 =item agentnum
310
311 =item charged
312
313 List reference of charged limits (exclusive).
314
315 =item owed
316
317 List reference of charged limits (exclusive).
318
319 =item open
320
321 flag, return open invoices only
322
323 =item net
324
325 flag, return net invoices only
326
327 =item days
328
329 =item newest_percust
330
331 =back
332
333 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
334
335 =cut
336
337 sub search_sql_where {
338   my($class, $param) = @_;
339   #if ( $DEBUG ) {
340   #  warn "$me search_sql_where called with params: \n".
341   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
342   #}
343
344   my @search = ();
345
346   #agentnum
347   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
348     push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
349   }
350
351 #  #refnum
352 #  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
353 #    push @search, "cust_main.refnum = $1";
354 #  }
355
356   #prospectnum
357   if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
358     push @search, "quotation.prospectnum = $1";
359   }
360
361   #custnum
362   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
363     push @search, "cust_bill.custnum = $1";
364   }
365
366   #_date
367   if ( $param->{_date} ) {
368     my($beginning, $ending) = @{$param->{_date}};
369
370     push @search, "quotation._date >= $beginning",
371                   "quotation._date <  $ending";
372   }
373
374   #quotationnum
375   if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
376     push @search, "quotation.quotationnum >= $1";
377   }
378   if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
379     push @search, "quotation.quotationnum <= $1";
380   }
381
382 #  #charged
383 #  if ( $param->{charged} ) {
384 #    my @charged = ref($param->{charged})
385 #                    ? @{ $param->{charged} }
386 #                    : ($param->{charged});
387 #
388 #    push @search, map { s/^charged/cust_bill.charged/; $_; }
389 #                      @charged;
390 #  }
391
392   my $owed_sql = FS::cust_bill->owed_sql;
393
394   #days
395   push @search, "quotation._date < ". (time-86400*$param->{'days'})
396     if $param->{'days'};
397
398   #agent virtualization
399   my $curuser = $FS::CurrentUser::CurrentUser;
400   #false laziness w/search/quotation.html
401   push @search,' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
402                '   OR '. $curuser->agentnums_sql( table=>'cust_main' ).
403                ' )    ';
404
405   join(' AND ', @search );
406
407 }
408
409 =back
410
411 =head1 BUGS
412
413 =head1 SEE ALSO
414
415 L<FS::Record>, schema.html from the base documentation.
416
417 =cut
418
419 1;
420