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