0e6b4e7d09f44a95a84bf7a1af9e80491b65b849
[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::CurrentUser;
7 use FS::UID qw( dbh );
8 use FS::Maketext qw( emt );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Conf;
11 use FS::cust_main;
12 use FS::cust_pkg;
13 use FS::prospect_main;
14 use FS::quotation_pkg;
15 use FS::quotation_pkg_tax;
16 use FS::type_pkgs;
17
18 =head1 NAME
19
20 FS::quotation - Object methods for quotation records
21
22 =head1 SYNOPSIS
23
24   use FS::quotation;
25
26   $record = new FS::quotation \%hash;
27   $record = new FS::quotation { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37 =head1 DESCRIPTION
38
39 An FS::quotation object represents a quotation.  FS::quotation inherits from
40 FS::Record.  The following fields are currently supported:
41
42 =over 4
43
44 =item quotationnum
45
46 primary key
47
48 =item prospectnum
49
50 prospectnum
51
52 =item custnum
53
54 custnum
55
56 =item _date
57
58 _date
59
60 =item disabled
61
62 disabled
63
64 =item usernum
65
66 usernum
67
68
69 =back
70
71 =head1 METHODS
72
73 =over 4
74
75 =item new HASHREF
76
77 Creates a new quotation.  To add the quotation to the database, see L<"insert">.
78
79 Note that this stores the hash reference, not a distinct copy of the hash it
80 points to.  You can ask the object for a copy with the I<hash> method.
81
82 =cut
83
84 sub table { 'quotation'; }
85 sub notice_name { 'Quotation'; }
86 sub template_conf { 'quotation_'; }
87
88 =item insert
89
90 Adds this record to the database.  If there is an error, returns the error,
91 otherwise returns false.
92
93 =item delete
94
95 Delete this record from the database.
96
97 =item replace OLD_RECORD
98
99 Replaces the OLD_RECORD with this one in the database.  If there is an error,
100 returns the error, otherwise returns false.
101
102 =item check
103
104 Checks all fields to make sure this is a valid quotation.  If there is
105 an error, returns the error, otherwise returns false.  Called by the insert
106 and replace methods.
107
108 =cut
109
110 sub check {
111   my $self = shift;
112
113   my $error = 
114     $self->ut_numbern('quotationnum')
115     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
116     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
117     || $self->ut_numbern('_date')
118     || $self->ut_enum('disabled', [ '', 'Y' ])
119     || $self->ut_numbern('usernum')
120   ;
121   return $error if $error;
122
123   $self->_date(time) unless $self->_date;
124
125   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
126
127   return 'prospectnum or custnum must be specified'
128     if ! $self->prospectnum
129     && ! $self->custnum;
130
131   $self->SUPER::check;
132 }
133
134 =item prospect_main
135
136 =cut
137
138 sub prospect_main {
139   my $self = shift;
140   qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
141 }
142
143 =item cust_main
144
145 =cut
146
147 sub cust_main {
148   my $self = shift;
149   qsearchs('cust_main', { 'custnum' => $self->custnum } );
150 }
151
152 =item cust_bill_pkg
153
154 =cut
155
156 sub cust_bill_pkg { #actually quotation_pkg objects
157   my $self = shift;
158   qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
159 }
160
161 =item total_setup
162
163 =cut
164
165 sub total_setup {
166   my $self = shift;
167   $self->_total('setup');
168 }
169
170 =item total_recur [ FREQ ]
171
172 =cut
173
174 sub total_recur {
175   my $self = shift;
176 #=item total_recur [ FREQ ]
177   #my $freq = @_ ? shift : '';
178   $self->_total('recur');
179 }
180
181 sub _total {
182   my( $self, $method ) = @_;
183
184   my $total = 0;
185   $total += $_->$method() for $self->cust_bill_pkg;
186   sprintf('%.2f', $total);
187
188 }
189
190 sub email {
191   my $self = shift;
192   my $opt = shift || {};
193   if ($opt and !ref($opt)) {
194     die ref($self). '->email called with positional parameters';
195   }
196
197   my $conf = $self->conf;
198
199   my $from = delete $opt->{from};
200
201   # this is where we set the From: address
202   $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
203         ||  $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
204   $self->SUPER::email( {
205     'from' => $from,
206     %$opt,
207   });
208
209 }
210
211 sub email_subject {
212   my $self = shift;
213
214   my $subject =
215     $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
216       || 'Quotation';
217
218   #my $cust_main = $self->cust_main;
219   #my $name = $cust_main->name;
220   #my $name_short = $cust_main->name_short;
221   #my $invoice_number = $self->invnum;
222   #my $invoice_date = $self->_date_pretty;
223
224   eval qq("$subject");
225 }
226
227 =item cust_or_prosect
228
229 =cut
230
231 sub cust_or_prospect {
232   my $self = shift;
233   $self->custnum ? $self->cust_main : $self->prospect_main;
234 }
235
236 =item cust_or_prospect_label_link P
237
238 HTML links to either the customer or prospect.
239
240 Returns a list consisting of two elements.  The first is a text label for the
241 link, and the second is the URL.
242
243 =cut
244
245 sub cust_or_prospect_label_link {
246   my( $self, $p ) = @_;
247
248   if ( my $custnum = $self->custnum ) {
249     my $display_custnum = $self->cust_main->display_custnum;
250     my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
251                    ? '#quotations'
252                    : ';show=quotations';
253     (
254       emt("View this customer (#[_1])",$display_custnum) =>
255         "${p}view/cust_main.cgi?custnum=$custnum$target"
256     );
257   } elsif ( my $prospectnum = $self->prospectnum ) {
258     (
259       emt("View this prospect (#[_1])",$prospectnum) =>
260         "${p}view/prospect_main.html?$prospectnum"
261     );
262   } else { #die?
263     ( '', '' );
264   }
265
266 }
267
268 sub _items_tax {
269   ();
270 }
271
272 sub _items_nontax {
273   shift->cust_bill_pkg;
274 }
275
276 sub _items_total {
277   my $self = shift;
278   $self->quotationnum =~ /^(\d+)$/ or return ();
279
280   my @items;
281
282   # show taxes in here also; the setup/recurring breakdown is different
283   # from what Template_Mixin expects
284   my @setup_tax = qsearch({
285       select      => 'itemdesc, SUM(setup_amount) as setup_amount',
286       table       => 'quotation_pkg_tax',
287       addl_from   => ' JOIN quotation_pkg USING (quotationpkgnum) ',
288       extra_sql   => ' WHERE quotationnum = '.$1,
289       order_by    => ' GROUP BY itemdesc HAVING SUM(setup_amount) > 0' .
290                      ' ORDER BY itemdesc',
291   });
292   # recurs need to be grouped by frequency, and to have a pkgpart
293   my @recur_tax = qsearch({
294       select      => 'freq, itemdesc, SUM(recur_amount) as recur_amount, MAX(pkgpart) as pkgpart',
295       table       => 'quotation_pkg_tax',
296       addl_from   => ' JOIN quotation_pkg USING (quotationpkgnum)'.
297                      ' JOIN part_pkg USING (pkgpart)',
298       extra_sql   => ' WHERE quotationnum = '.$1,
299       order_by    => ' GROUP BY freq, itemdesc HAVING SUM(recur_amount) > 0' .
300                      ' ORDER BY freq, itemdesc',
301   });
302
303   my $total_setup = $self->total_setup;
304   foreach my $pkg_tax (@setup_tax) {
305     if ($pkg_tax->setup_amount > 0) {
306       $total_setup += $pkg_tax->setup_amount;
307       push @items, {
308         'total_item'    => $pkg_tax->itemdesc . ' ' . $self->mt('(setup)'),
309         'total_amount'  => $pkg_tax->setup_amount,
310       };
311     }
312   }
313
314   if ( $total_setup > 0 ) {
315     push @items, {
316       'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
317       'total_amount' => sprintf('%.2f',$total_setup),
318       'break_after'  => ( scalar(@recur_tax) ? 1 : 0 )
319     };
320   }
321
322   #could/should add up the different recurring frequencies on lines of their own
323   # but this will cover the 95% cases for now
324   my $total_recur = $self->total_recur;
325   # label these with the frequency
326   foreach my $pkg_tax (@recur_tax) {
327     if ($pkg_tax->recur_amount > 0) {
328       $total_recur += $pkg_tax->recur_amount;
329       # an arbitrary part_pkg, but with the right frequency
330       # XXX localization
331       my $part_pkg = qsearchs('part_pkg', { pkgpart => $pkg_tax->pkgpart });
332       push @items, {
333         'total_item'    => $pkg_tax->itemdesc . ' (' .  $part_pkg->freq_pretty . ')',
334         'total_amount'  => $pkg_tax->recur_amount,
335       };
336     }
337   }
338
339   if ( $total_recur > 0 ) {
340     push @items, {
341       'total_item'   => $self->mt('Total Recurring'),
342       'total_amount' => sprintf('%.2f',$total_recur),
343       'break_after'  => 1,
344     };
345     # show 'first payment' line (setup + recur) if there are no prorated 
346     # packages included
347     my $disable_total = 0;
348     foreach my $quotation_pkg ($self->quotation_pkg) {
349       my $part_pkg = $quotation_pkg->part_pkg;
350       if (   $part_pkg->plan =~ /^prorate/
351           or $part_pkg->plan eq 'agent'
352           or $part_pkg->plan =~ /^torrus/
353           or $part_pkg->option('sync_bill_date')
354           or $part_pkg->option('recur_method') eq 'prorate' ) {
355         $disable_total = 1;
356         last;
357       }
358     }
359     if (!$disable_total) {
360       push @items, {
361         'total_item'   => $self->mt('First payment'),
362         'total_amount' => sprintf('%.2f', $total_setup + $total_recur),
363         'break_after'  => 1,
364       };
365     }
366   }
367
368   return @items;
369
370 }
371
372 =item enable_previous
373
374 =cut
375
376 sub enable_previous { 0 }
377
378 =item convert_cust_main
379
380 If this quotation already belongs to a customer, then returns that customer, as
381 an FS::cust_main object.
382
383 Otherwise, creates a new customer (FS::cust_main object and record, and
384 associated) based on this quotation's prospect, then orders this quotation's
385 packages as real packages for the customer.
386
387 If there is an error, returns an error message, otherwise, returns the
388 newly-created FS::cust_main object.
389
390 =cut
391
392 sub convert_cust_main {
393   my $self = shift;
394
395   my $cust_main = $self->cust_main;
396   return $cust_main if $cust_main; #already converted, don't again
397
398   my $oldAutoCommit = $FS::UID::AutoCommit;
399   local $FS::UID::AutoCommit = 0;
400   my $dbh = dbh;
401
402   $cust_main = $self->prospect_main->convert_cust_main;
403   unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
404     $dbh->rollback if $oldAutoCommit;
405     return $cust_main;
406   }
407
408   $self->prospectnum('');
409   $self->custnum( $cust_main->custnum );
410   my $error = $self->replace || $self->order;
411   if ( $error ) {
412     $dbh->rollback if $oldAutoCommit;
413     return $error;
414   }
415
416   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
417
418   $cust_main;
419
420 }
421
422 =item order
423
424 This method is for use with quotations which are already associated with a customer.
425
426 Orders this quotation's packages as real packages for the customer.
427
428 If there is an error, returns an error message, otherwise returns false.
429
430 =cut
431
432 sub order {
433   my $self = shift;
434
435   tie my %all_cust_pkg, 'Tie::RefHash';
436   foreach my $quotation_pkg ($self->quotation_pkg) {
437     my $cust_pkg = FS::cust_pkg->new;
438     foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
439       $cust_pkg->set( $_, $quotation_pkg->get($_) );
440     }
441
442     # currently only one discount each
443     my ($pkg_discount) = $quotation_pkg->quotation_pkg_discount;
444     if ( $pkg_discount ) {
445       $cust_pkg->set('discountnum', $pkg_discount->discountnum);
446     }
447
448     $all_cust_pkg{$cust_pkg} = []; # no services
449   }
450
451   $self->cust_main->order_pkgs( \%all_cust_pkg );
452
453 }
454
455 =item quotation_pkg
456
457 =cut
458
459 sub quotation_pkg {
460   my $self = shift;
461   qsearch('quotation_pkg', { 'quotationnum' => $self->quotationnum } );
462 }
463
464 =item charge
465
466 One-time charges, like FS::cust_main::charge()
467
468 =cut
469
470 #super false laziness w/cust_main::charge
471 sub charge {
472   my $self = shift;
473   my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
474   my ( $pkg, $comment, $additional );
475   my ( $setuptax, $taxclass );   #internal taxes
476   my ( $taxproduct, $override ); #vendor (CCH) taxes
477   my $no_auto = '';
478   my $cust_pkg_ref = '';
479   my ( $bill_now, $invoice_terms ) = ( 0, '' );
480   my $locationnum;
481   if ( ref( $_[0] ) ) {
482     $amount     = $_[0]->{amount};
483     $setup_cost = $_[0]->{setup_cost};
484     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
485     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
486     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
487     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
488     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
489                                            : '$'. sprintf("%.2f",$amount);
490     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
491     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
492     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
493     $additional = $_[0]->{additional} || [];
494     $taxproduct = $_[0]->{taxproductnum};
495     $override   = { '' => $_[0]->{tax_override} };
496     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
497     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
498     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
499     $locationnum = $_[0]->{locationnum};
500   } else {
501     $amount     = shift;
502     $setup_cost = '';
503     $quantity   = 1;
504     $start_date = '';
505     $pkg        = @_ ? shift : 'One-time charge';
506     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
507     $setuptax   = '';
508     $taxclass   = @_ ? shift : '';
509     $additional = [];
510   }
511
512   local $SIG{HUP} = 'IGNORE';
513   local $SIG{INT} = 'IGNORE';
514   local $SIG{QUIT} = 'IGNORE';
515   local $SIG{TERM} = 'IGNORE';
516   local $SIG{TSTP} = 'IGNORE';
517   local $SIG{PIPE} = 'IGNORE';
518
519   my $oldAutoCommit = $FS::UID::AutoCommit;
520   local $FS::UID::AutoCommit = 0;
521   my $dbh = dbh;
522
523   my $part_pkg = new FS::part_pkg ( {
524     'pkg'           => $pkg,
525     'comment'       => $comment,
526     'plan'          => 'flat',
527     'freq'          => 0,
528     'disabled'      => 'Y',
529     'classnum'      => ( $classnum ? $classnum : '' ),
530     'setuptax'      => $setuptax,
531     'taxclass'      => $taxclass,
532     'taxproductnum' => $taxproduct,
533     'setup_cost'    => $setup_cost,
534   } );
535
536   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
537                         ( 0 .. @$additional - 1 )
538                   ),
539                   'additional_count' => scalar(@$additional),
540                   'setup_fee' => $amount,
541                 );
542
543   my $error = $part_pkg->insert( options       => \%options,
544                                  tax_overrides => $override,
545                                );
546   if ( $error ) {
547     $dbh->rollback if $oldAutoCommit;
548     return $error;
549   }
550
551   my $pkgpart = $part_pkg->pkgpart;
552
553   #DIFF
554   my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
555
556   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
557     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
558     $error = $type_pkgs->insert;
559     if ( $error ) {
560       $dbh->rollback if $oldAutoCommit;
561       return $error;
562     }
563   }
564
565   #except for DIFF, eveything above is idential to cust_main version
566   #but below is our own thing pretty much (adding a quotation package instead
567   # of ordering a customer package, no "bill now")
568
569   my $quotation_pkg = new FS::quotation_pkg ( {
570     'quotationnum'  => $self->quotationnum,
571     'pkgpart'       => $pkgpart,
572     'quantity'      => $quantity,
573     #'start_date' => $start_date,
574     #'no_auto'    => $no_auto,
575     'locationnum'=> $locationnum,
576   } );
577
578   $error = $quotation_pkg->insert;
579   if ( $error ) {
580     $dbh->rollback if $oldAutoCommit;
581     return $error;
582   #} elsif ( $cust_pkg_ref ) {
583   #  ${$cust_pkg_ref} = $cust_pkg;
584   }
585
586   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
587   return '';
588
589 }
590
591 =item disable
592
593 Disables this quotation (sets disabled to Y, which hides the quotation on
594 prospects and customers).
595
596 If there is an error, returns an error message, otherwise returns false.
597
598 =cut
599
600 sub disable {
601   my $self = shift;
602   $self->disabled('Y');
603   $self->replace();
604 }
605
606 =item enable
607
608 Enables this quotation.
609
610 If there is an error, returns an error message, otherwise returns false.
611
612 =cut
613
614 sub enable {
615   my $self = shift;
616   $self->disabled('');
617   $self->replace();
618 }
619
620 =item estimate
621
622 Calculates current prices for all items on this quotation, including 
623 discounts and taxes, and updates the quotation_pkg records accordingly.
624
625 =cut
626
627 sub estimate {
628   my $self = shift;
629   my $conf = FS::Conf->new;
630
631   my $dbh = dbh;
632   my $oldAutoCommit = $FS::UID::AutoCommit;
633   local $FS::UID::AutoCommit = 0;
634
635   # bring individual items up to date (set setup/recur and discounts)
636   my @quotation_pkg = $self->quotation_pkg;
637   foreach my $pkg (@quotation_pkg) {
638     my $error = $pkg->estimate;
639     if ($error) {
640       $dbh->rollback if $oldAutoCommit;
641       die "error calculating estimate for pkgpart " . $pkg->pkgpart.": $error\n";
642     }
643
644     # delete old tax records
645     foreach my $quotation_pkg_tax ($pkg->quotation_pkg_tax) {
646       $error = $quotation_pkg_tax->delete;
647       if ( $error ) {
648         $dbh->rollback if $oldAutoCommit;
649         die "error flushing tax records for pkgpart ". $pkg->pkgpart.": $error\n";
650       }
651     }
652   }
653
654   # annoyingly duplicates handle_taxes--fix this in 4.x 
655   if ( $conf->exists('enable_taxproducts') ) {
656     warn "can't calculate external taxes for quotations yet\n";
657     # then we're done
658     return;
659   }
660
661   my %taxnum_exemptions; # for monthly exemptions; as yet unused
662
663   foreach my $pkg (@quotation_pkg) {
664     my $location = $pkg->cust_location;
665
666     my $part_item = $pkg->part_pkg; # we don't have fees on these yet
667     my @loc_keys = qw( district city county state country);
668     my %taxhash = map { $_ => $location->$_ } @loc_keys;
669     $taxhash{'taxclass'} = $part_item->taxclass;
670     my @taxes;
671     my %taxhash_elim = %taxhash;
672     my @elim = qw( district city county state );
673     do {
674       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
675       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
676         #then try a match without taxclass
677         my %no_taxclass = %taxhash_elim;
678         $no_taxclass{ 'taxclass' } = '';
679         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
680       }
681     
682       $taxhash_elim{ shift(@elim) } = '';
683     } while ( !scalar(@taxes) && scalar(@elim) );
684
685     foreach my $tax_def (@taxes) {
686       my $taxnum = $tax_def->taxnum;
687       $taxnum_exemptions{$taxnum} ||= [];
688
689       # XXX do some kind of equivalent to set_exemptions here
690       # but for now just declare that there are no exemptions,
691       # and then hack the taxable amounts if the package def
692       # excludes setup/recur
693       $pkg->set('cust_tax_exempt_pkg', []);
694
695       if ( $part_item->setuptax or $tax_def->setuptax ) {
696         $pkg->set('unitsetup', 0);
697       }
698       if ( $part_item->recurtax or $tax_def->recurtax ) {
699         $pkg->set('unitrecur', 0);
700       }
701
702       my %taxline;
703       foreach my $pass (qw(first recur)) {
704         if ($pass eq 'recur') {
705           $pkg->set('unitsetup', 0);
706         }
707
708         my $taxline = $tax_def->taxline(
709           [ $pkg ],
710           exemptions => $taxnum_exemptions{$taxnum}
711         );
712         if ($taxline and !ref($taxline)) {
713           $dbh->rollback if $oldAutoCommit;
714           die "error calculating '".$tax_def->taxname .
715               "' for pkgpart '".$pkg->pkgpart."': $taxline\n";
716         }
717         $taxline{$pass} = $taxline;
718       }
719
720       my $quotation_pkg_tax = FS::quotation_pkg_tax->new({
721           quotationpkgnum => $pkg->quotationpkgnum,
722           itemdesc        => ($tax_def->taxname || 'Tax'),
723           taxnum          => $taxnum,
724           taxtype         => ref($tax_def),
725       });
726       my $setup_amount = 0;
727       my $recur_amount = 0;
728       if ($taxline{first}) {
729         $setup_amount = $taxline{first}->setup; # "first cycle", not setup
730       }
731       if ($taxline{recur}) {
732         $recur_amount = $taxline{recur}->setup;
733         $setup_amount -= $recur_amount; # to get the actual setup amount
734       }
735       if ( $recur_amount > 0 or $setup_amount > 0 ) {
736         $quotation_pkg_tax->set('setup_amount', sprintf('%.2f', $setup_amount));
737         $quotation_pkg_tax->set('recur_amount', sprintf('%.2f', $recur_amount));
738
739         my $error = $quotation_pkg_tax->insert;
740         if ($error) {
741           $dbh->rollback if $oldAutoCommit;
742           die "error recording '".$tax_def->taxname .
743               "' for pkgpart '".$pkg->pkgpart."': $error\n";
744         } # if $error
745       } # else there are no non-zero taxes; continue
746     } # foreach $tax_def
747   } # foreach $pkg
748
749   $dbh->commit if $oldAutoCommit;
750   '';
751 }
752
753 =back
754
755 =head1 CLASS METHODS
756
757 =over 4
758
759
760 =item search_sql_where HASHREF
761
762 Class method which returns an SQL WHERE fragment to search for parameters
763 specified in HASHREF.  Valid parameters are
764
765 =over 4
766
767 =item _date
768
769 List reference of start date, end date, as UNIX timestamps.
770
771 =item invnum_min
772
773 =item invnum_max
774
775 =item agentnum
776
777 =item charged
778
779 List reference of charged limits (exclusive).
780
781 =item owed
782
783 List reference of charged limits (exclusive).
784
785 =item open
786
787 flag, return open invoices only
788
789 =item net
790
791 flag, return net invoices only
792
793 =item days
794
795 =item newest_percust
796
797 =back
798
799 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
800
801 =cut
802
803 sub search_sql_where {
804   my($class, $param) = @_;
805   #if ( $DEBUG ) {
806   #  warn "$me search_sql_where called with params: \n".
807   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
808   #}
809
810   my @search = ();
811
812   #agentnum
813   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
814     push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
815   }
816
817 #  #refnum
818 #  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
819 #    push @search, "cust_main.refnum = $1";
820 #  }
821
822   #prospectnum
823   if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
824     push @search, "quotation.prospectnum = $1";
825   }
826
827   #custnum
828   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
829     push @search, "cust_bill.custnum = $1";
830   }
831
832   #_date
833   if ( $param->{_date} ) {
834     my($beginning, $ending) = @{$param->{_date}};
835
836     push @search, "quotation._date >= $beginning",
837                   "quotation._date <  $ending";
838   }
839
840   #quotationnum
841   if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
842     push @search, "quotation.quotationnum >= $1";
843   }
844   if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
845     push @search, "quotation.quotationnum <= $1";
846   }
847
848 #  #charged
849 #  if ( $param->{charged} ) {
850 #    my @charged = ref($param->{charged})
851 #                    ? @{ $param->{charged} }
852 #                    : ($param->{charged});
853 #
854 #    push @search, map { s/^charged/cust_bill.charged/; $_; }
855 #                      @charged;
856 #  }
857
858   my $owed_sql = FS::cust_bill->owed_sql;
859
860   #days
861   push @search, "quotation._date < ". (time-86400*$param->{'days'})
862     if $param->{'days'};
863
864   #agent virtualization
865   my $curuser = $FS::CurrentUser::CurrentUser;
866   #false laziness w/search/quotation.html
867   push @search,' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
868                '   OR '. $curuser->agentnums_sql( table=>'cust_main' ).
869                ' )    ';
870
871   join(' AND ', @search );
872
873 }
874
875 =item _items_pkg
876
877 Return line item hashes for each package on this quotation. Differs from the
878 base L<FS::Template_Mixin> version in that it recalculates each quoted package
879 first, and doesn't implement the "condensed" option.
880
881 =cut
882
883 sub _items_pkg {
884   my ($self, %options) = @_;
885   $self->estimate;
886   # run it through the Template_Mixin engine
887   return $self->_items_cust_bill_pkg([ $self->quotation_pkg ], %options);
888 }
889
890 =back
891
892 =head1 BUGS
893
894 =head1 SEE ALSO
895
896 L<FS::Record>, schema.html from the base documentation.
897
898 =cut
899
900 1;
901