2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
8 use FS::Maketext qw( emt );
9 use FS::Record qw( qsearch qsearchs );
12 use FS::prospect_main;
13 use FS::quotation_pkg;
18 FS::quotation - Object methods for quotation records
24 $record = new FS::quotation \%hash;
25 $record = new FS::quotation { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::quotation object represents a quotation. FS::quotation inherits from
38 FS::Record. The following fields are currently supported:
75 Creates a new quotation. To add the quotation to the database, see L<"insert">.
77 Note that this stores the hash reference, not a distinct copy of the hash it
78 points to. You can ask the object for a copy with the I<hash> method.
82 sub table { 'quotation'; }
83 sub notice_name { 'Quotation'; }
84 sub template_conf { 'quotation_'; }
88 Adds this record to the database. If there is an error, returns the error,
89 otherwise returns false.
93 Delete this record from the database.
95 =item replace OLD_RECORD
97 Replaces the OLD_RECORD with this one in the database. If there is an error,
98 returns the error, otherwise returns false.
102 Checks all fields to make sure this is a valid quotation. If there is
103 an error, returns the error, otherwise returns false. Called by the insert
112 $self->ut_numbern('quotationnum')
113 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
114 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
115 || $self->ut_numbern('_date')
116 || $self->ut_enum('disabled', [ '', 'Y' ])
117 || $self->ut_numbern('usernum')
119 return $error if $error;
121 $self->_date(time) unless $self->_date;
123 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
125 return 'prospectnum or custnum must be specified'
126 if ! $self->prospectnum
138 qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
147 qsearchs('cust_main', { 'custnum' => $self->custnum } );
154 sub cust_bill_pkg { #actually quotation_pkg objects
156 qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
165 $self->_total('setup');
168 =item total_recur [ FREQ ]
174 #=item total_recur [ FREQ ]
175 #my $freq = @_ ? shift : '';
176 $self->_total('recur');
180 my( $self, $method ) = @_;
183 $total += $_->$method() for $self->cust_bill_pkg;
184 sprintf('%.2f', $total);
190 my $opt = shift || {};
191 if ($opt and !ref($opt)) {
192 die ref($self). '->email called with positional parameters';
195 my $conf = $self->conf;
197 my $from = delete $opt->{from};
199 # this is where we set the From: address
200 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
201 || ($conf->config('invoice_from_name', $self->cust_or_prospect->agentnum ) ?
202 $conf->config('invoice_from_name', $self->cust_or_prospect->agentnum ) . ' <' .
203 $conf->config('invoice_from', $self->cust_or_prospect->agentnum ) . '>' :
204 $conf->config('invoice_from', $self->cust_or_prospect->agentnum ));
205 $self->SUPER::email( {
216 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
219 #my $cust_main = $self->cust_main;
220 #my $name = $cust_main->name;
221 #my $name_short = $cust_main->name_short;
222 #my $invoice_number = $self->invnum;
223 #my $invoice_date = $self->_date_pretty;
228 =item cust_or_prosect
232 sub cust_or_prospect {
234 $self->custnum ? $self->cust_main : $self->prospect_main;
237 =item cust_or_prospect_label_link P
239 HTML links to either the customer or prospect.
241 Returns a list consisting of two elements. The first is a text label for the
242 link, and the second is the URL.
246 sub cust_or_prospect_label_link {
247 my( $self, $p ) = @_;
249 if ( my $custnum = $self->custnum ) {
250 my $display_custnum = $self->cust_main->display_custnum;
251 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
253 : ';show=quotations';
255 emt("View this customer (#[_1])",$display_custnum) =>
256 "${p}view/cust_main.cgi?custnum=$custnum$target"
258 } elsif ( my $prospectnum = $self->prospectnum ) {
260 emt("View this prospect (#[_1])",$prospectnum) =>
261 "${p}view/prospect_main.html?$prospectnum"
269 #prevent things from falsely showing up as taxes, at least until we support
270 # quoting tax amounts..
275 shift->cust_bill_pkg;
279 my( $self, $total_items ) = @_;
281 if ( $self->total_setup > 0 ) {
282 push @$total_items, {
283 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
284 'total_amount' => $self->total_setup,
288 #could/should add up the different recurring frequencies on lines of their own
289 # but this will cover the 95% cases for now
290 if ( $self->total_recur > 0 ) {
291 push @$total_items, {
292 'total_item' => $self->mt('Total Recurring'),
293 'total_amount' => $self->total_recur,
299 =item enable_previous
303 sub enable_previous { 0 }
305 =item convert_cust_main
307 If this quotation already belongs to a customer, then returns that customer, as
308 an FS::cust_main object.
310 Otherwise, creates a new customer (FS::cust_main object and record, and
311 associated) based on this quotation's prospect, then orders this quotation's
312 packages as real packages for the customer.
314 If there is an error, returns an error message, otherwise, returns the
315 newly-created FS::cust_main object.
319 sub convert_cust_main {
322 my $cust_main = $self->cust_main;
323 return $cust_main if $cust_main; #already converted, don't again
325 my $oldAutoCommit = $FS::UID::AutoCommit;
326 local $FS::UID::AutoCommit = 0;
329 $cust_main = $self->prospect_main->convert_cust_main;
330 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
331 $dbh->rollback if $oldAutoCommit;
335 $self->prospectnum('');
336 $self->custnum( $cust_main->custnum );
337 my $error = $self->replace || $self->order;
339 $dbh->rollback if $oldAutoCommit;
343 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
351 This method is for use with quotations which are already associated with a customer.
353 Orders this quotation's packages as real packages for the customer.
355 If there is an error, returns an error message, otherwise returns false.
362 tie my %cust_pkg, 'Tie::RefHash',
363 map { FS::cust_pkg->new({ pkgpart => $_->pkgpart,
364 quantity => $_->quantity,
368 $self->quotation_pkg ;
370 $self->cust_main->order_pkgs( \%cust_pkg );
380 qsearch('quotation_pkg', { 'quotationnum' => $self->quotationnum } );
385 One-time charges, like FS::cust_main::charge()
389 #super false laziness w/cust_main::charge
392 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
393 my ( $pkg, $comment, $additional );
394 my ( $setuptax, $taxclass ); #internal taxes
395 my ( $taxproduct, $override ); #vendor (CCH) taxes
397 my $cust_pkg_ref = '';
398 my ( $bill_now, $invoice_terms ) = ( 0, '' );
400 if ( ref( $_[0] ) ) {
401 $amount = $_[0]->{amount};
402 $setup_cost = $_[0]->{setup_cost};
403 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
404 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
405 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
406 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
407 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
408 : '$'. sprintf("%.2f",$amount);
409 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
410 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
411 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
412 $additional = $_[0]->{additional} || [];
413 $taxproduct = $_[0]->{taxproductnum};
414 $override = { '' => $_[0]->{tax_override} };
415 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
416 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
417 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
418 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
424 $pkg = @_ ? shift : 'One-time charge';
425 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
427 $taxclass = @_ ? shift : '';
431 local $SIG{HUP} = 'IGNORE';
432 local $SIG{INT} = 'IGNORE';
433 local $SIG{QUIT} = 'IGNORE';
434 local $SIG{TERM} = 'IGNORE';
435 local $SIG{TSTP} = 'IGNORE';
436 local $SIG{PIPE} = 'IGNORE';
438 my $oldAutoCommit = $FS::UID::AutoCommit;
439 local $FS::UID::AutoCommit = 0;
442 my $part_pkg = new FS::part_pkg ( {
444 'comment' => $comment,
448 'classnum' => ( $classnum ? $classnum : '' ),
449 'setuptax' => $setuptax,
450 'taxclass' => $taxclass,
451 'taxproductnum' => $taxproduct,
452 'setup_cost' => $setup_cost,
455 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
456 ( 0 .. @$additional - 1 )
458 'additional_count' => scalar(@$additional),
459 'setup_fee' => $amount,
462 my $error = $part_pkg->insert( options => \%options,
463 tax_overrides => $override,
466 $dbh->rollback if $oldAutoCommit;
470 my $pkgpart = $part_pkg->pkgpart;
473 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
475 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
476 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
477 $error = $type_pkgs->insert;
479 $dbh->rollback if $oldAutoCommit;
484 #except for DIFF, eveything above is idential to cust_main version
485 #but below is our own thing pretty much (adding a quotation package instead
486 # of ordering a customer package, no "bill now")
488 my $quotation_pkg = new FS::quotation_pkg ( {
489 'quotationnum' => $self->quotationnum,
490 'pkgpart' => $pkgpart,
491 'quantity' => $quantity,
492 #'start_date' => $start_date,
493 #'no_auto' => $no_auto,
494 'locationnum'=> $locationnum,
497 $error = $quotation_pkg->insert;
499 $dbh->rollback if $oldAutoCommit;
501 #} elsif ( $cust_pkg_ref ) {
502 # ${$cust_pkg_ref} = $cust_pkg;
505 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 Disables this quotation (sets disabled to Y, which hides the quotation on
513 prospects and customers).
515 If there is an error, returns an error message, otherwise returns false.
521 $self->disabled('Y');
527 Enables this quotation.
529 If there is an error, returns an error message, otherwise returns false.
546 =item search_sql_where HASHREF
548 Class method which returns an SQL WHERE fragment to search for parameters
549 specified in HASHREF. Valid parameters are
555 List reference of start date, end date, as UNIX timestamps.
565 List reference of charged limits (exclusive).
569 List reference of charged limits (exclusive).
573 flag, return open invoices only
577 flag, return net invoices only
585 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
589 sub search_sql_where {
590 my($class, $param) = @_;
592 # warn "$me search_sql_where called with params: \n".
593 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
599 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
600 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
604 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
605 # push @search, "cust_main.refnum = $1";
609 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
610 push @search, "quotation.prospectnum = $1";
614 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
615 push @search, "cust_bill.custnum = $1";
619 if ( $param->{_date} ) {
620 my($beginning, $ending) = @{$param->{_date}};
622 push @search, "quotation._date >= $beginning",
623 "quotation._date < $ending";
627 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
628 push @search, "quotation.quotationnum >= $1";
630 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
631 push @search, "quotation.quotationnum <= $1";
635 # if ( $param->{charged} ) {
636 # my @charged = ref($param->{charged})
637 # ? @{ $param->{charged} }
638 # : ($param->{charged});
640 # push @search, map { s/^charged/cust_bill.charged/; $_; }
644 my $owed_sql = FS::cust_bill->owed_sql;
647 push @search, "quotation._date < ". (time-86400*$param->{'days'})
650 #agent virtualization
651 my $curuser = $FS::CurrentUser::CurrentUser;
652 #false laziness w/search/quotation.html
653 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
654 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
657 join(' AND ', @search );
667 L<FS::Record>, schema.html from the base documentation.