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