fix prospet quotation creation, fallout from #25561
[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   return 'prospectnum or custnum must be specified'
124     if ! $self->prospectnum
125     && ! $self->custnum;
126
127   $self->SUPER::check;
128 }
129
130 =item prospect_main
131
132 =cut
133
134 sub prospect_main {
135   my $self = shift;
136   qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
137 }
138
139 =item cust_main
140
141 =cut
142
143 sub cust_main {
144   my $self = shift;
145   qsearchs('cust_main', { 'custnum' => $self->custnum } );
146 }
147
148 =item cust_bill_pkg
149
150 =cut
151
152 sub cust_bill_pkg { #actually quotation_pkg objects
153   my $self = shift;
154   qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
155 }
156
157 =item total_setup
158
159 =cut
160
161 sub total_setup {
162   my $self = shift;
163   $self->_total('setup');
164 }
165
166 =item total_recur [ FREQ ]
167
168 =cut
169
170 sub total_recur {
171   my $self = shift;
172 #=item total_recur [ FREQ ]
173   #my $freq = @_ ? shift : '';
174   $self->_total('recur');
175 }
176
177 sub _total {
178   my( $self, $method ) = @_;
179
180   my $total = 0;
181   $total += $_->$method() for $self->cust_bill_pkg;
182   sprintf('%.2f', $total);
183
184 }
185
186 #prevent things from falsely showing up as taxes, at least until we support
187 # quoting tax amounts..
188 sub _items_tax {
189   return ();
190 }
191 sub _items_nontax {
192   shift->cust_bill_pkg;
193 }
194
195 sub _items_total {
196   my( $self, $total_items ) = @_;
197
198   if ( $self->total_setup > 0 ) {
199     push @$total_items, {
200       'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
201       'total_amount' => $self->total_setup,
202     };
203   }
204
205   #could/should add up the different recurring frequencies on lines of their own
206   # but this will cover the 95% cases for now
207   if ( $self->total_recur > 0 ) {
208     push @$total_items, {
209       'total_item'   => $self->mt('Total Recurring'),
210       'total_amount' => $self->total_recur,
211     };
212   }
213
214 }
215
216 =item enable_previous
217
218 =cut
219
220 sub enable_previous { 0 }
221
222 =item convert_cust_main
223
224 If this quotation already belongs to a customer, then returns that customer, as
225 an FS::cust_main object.
226
227 Otherwise, creates a new customer (FS::cust_main object and record, and
228 associated) based on this quotation's prospect, then orders this quotation's
229 packages as real packages for the customer.
230
231 If there is an error, returns an error message, otherwise, returns the
232 newly-created FS::cust_main object.
233
234 =cut
235
236 sub convert_cust_main {
237   my $self = shift;
238
239   my $cust_main = $self->cust_main;
240   return $cust_main if $cust_main; #already converted, don't again
241
242   my $oldAutoCommit = $FS::UID::AutoCommit;
243   local $FS::UID::AutoCommit = 0;
244   my $dbh = dbh;
245
246   $cust_main = $self->prospect_main->convert_cust_main;
247   unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
248     $dbh->rollback if $oldAutoCommit;
249     return $cust_main;
250   }
251
252   $self->prospectnum('');
253   $self->custnum( $cust_main->custnum );
254   my $error = $self->replace || $self->order;
255   if ( $error ) {
256     $dbh->rollback if $oldAutoCommit;
257     return $error;
258   }
259
260   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
261
262   $cust_main;
263
264 }
265
266 =item order
267
268 This method is for use with quotations which are already associated with a customer.
269
270 Orders this quotation's packages as real packages for the customer.
271
272 If there is an error, returns an error message, otherwise returns false.
273
274 =cut
275
276 sub order {
277   my $self = shift;
278
279   tie my %cust_pkg, 'Tie::RefHash',
280     map { FS::cust_pkg->new({ pkgpart  => $_->pkgpart,
281                               quantity => $_->quantity,
282                            })
283             => [] #services
284         }
285       $self->quotation_pkg ;
286
287   $self->cust_main->order_pkgs( \%cust_pkg );
288
289 }
290
291 =back
292
293 =head1 CLASS METHODS
294
295 =over 4
296
297
298 =item search_sql_where HASHREF
299
300 Class method which returns an SQL WHERE fragment to search for parameters
301 specified in HASHREF.  Valid parameters are
302
303 =over 4
304
305 =item _date
306
307 List reference of start date, end date, as UNIX timestamps.
308
309 =item invnum_min
310
311 =item invnum_max
312
313 =item agentnum
314
315 =item charged
316
317 List reference of charged limits (exclusive).
318
319 =item owed
320
321 List reference of charged limits (exclusive).
322
323 =item open
324
325 flag, return open invoices only
326
327 =item net
328
329 flag, return net invoices only
330
331 =item days
332
333 =item newest_percust
334
335 =back
336
337 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
338
339 =cut
340
341 sub search_sql_where {
342   my($class, $param) = @_;
343   #if ( $DEBUG ) {
344   #  warn "$me search_sql_where called with params: \n".
345   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
346   #}
347
348   my @search = ();
349
350   #agentnum
351   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
352     push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
353   }
354
355 #  #refnum
356 #  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
357 #    push @search, "cust_main.refnum = $1";
358 #  }
359
360   #prospectnum
361   if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
362     push @search, "quotation.prospectnum = $1";
363   }
364
365   #custnum
366   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
367     push @search, "cust_bill.custnum = $1";
368   }
369
370   #_date
371   if ( $param->{_date} ) {
372     my($beginning, $ending) = @{$param->{_date}};
373
374     push @search, "quotation._date >= $beginning",
375                   "quotation._date <  $ending";
376   }
377
378   #quotationnum
379   if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
380     push @search, "quotation.quotationnum >= $1";
381   }
382   if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
383     push @search, "quotation.quotationnum <= $1";
384   }
385
386 #  #charged
387 #  if ( $param->{charged} ) {
388 #    my @charged = ref($param->{charged})
389 #                    ? @{ $param->{charged} }
390 #                    : ($param->{charged});
391 #
392 #    push @search, map { s/^charged/cust_bill.charged/; $_; }
393 #                      @charged;
394 #  }
395
396   my $owed_sql = FS::cust_bill->owed_sql;
397
398   #days
399   push @search, "quotation._date < ". (time-86400*$param->{'days'})
400     if $param->{'days'};
401
402   #agent virtualization
403   my $curuser = $FS::CurrentUser::CurrentUser;
404   #false laziness w/search/quotation.html
405   push @search,' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
406                '   OR '. $curuser->agentnums_sql( table=>'cust_main' ).
407                ' )    ';
408
409   join(' AND ', @search );
410
411 }
412
413 =back
414
415 =head1 BUGS
416
417 =head1 SEE ALSO
418
419 L<FS::Record>, schema.html from the base documentation.
420
421 =cut
422
423 1;
424