2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearchs );
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
140 sub cust_bill_pkg { #actually quotation_pkg objects
141 shift->quotation_pkg(@_);
150 $self->_total('setup');
153 =item total_recur [ FREQ ]
159 #=item total_recur [ FREQ ]
160 #my $freq = @_ ? shift : '';
161 $self->_total('recur');
165 my( $self, $method ) = @_;
168 $total += $_->$method() for $self->cust_bill_pkg;
169 sprintf('%.2f', $total);
175 my $opt = shift || {};
176 if ($opt and !ref($opt)) {
177 die ref($self). '->email called with positional parameters';
180 my $conf = $self->conf;
182 my $from = delete $opt->{from};
184 # this is where we set the From: address
185 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
186 || ($conf->config('invoice_from_name', $self->cust_or_prospect->agentnum ) ?
187 $conf->config('invoice_from_name', $self->cust_or_prospect->agentnum ) . ' <' .
188 $conf->config('invoice_from', $self->cust_or_prospect->agentnum ) . '>' :
189 $conf->config('invoice_from', $self->cust_or_prospect->agentnum ));
190 $self->SUPER::email( {
201 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
204 #my $cust_main = $self->cust_main;
205 #my $name = $cust_main->name;
206 #my $name_short = $cust_main->name_short;
207 #my $invoice_number = $self->invnum;
208 #my $invoice_date = $self->_date_pretty;
213 =item cust_or_prosect
217 sub cust_or_prospect {
219 $self->custnum ? $self->cust_main : $self->prospect_main;
222 =item cust_or_prospect_label_link P
224 HTML links to either the customer or prospect.
226 Returns a list consisting of two elements. The first is a text label for the
227 link, and the second is the URL.
231 sub cust_or_prospect_label_link {
232 my( $self, $p ) = @_;
234 if ( my $custnum = $self->custnum ) {
235 my $display_custnum = $self->cust_main->display_custnum;
236 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
238 : ';show=quotations';
240 emt("View this customer (#[_1])",$display_custnum) =>
241 "${p}view/cust_main.cgi?custnum=$custnum$target"
243 } elsif ( my $prospectnum = $self->prospectnum ) {
245 emt("View this prospect (#[_1])",$prospectnum) =>
246 "${p}view/prospect_main.html?$prospectnum"
254 #prevent things from falsely showing up as taxes, at least until we support
255 # quoting tax amounts..
260 shift->cust_bill_pkg;
264 my( $self, $total_items ) = @_;
266 if ( $self->total_setup > 0 ) {
267 push @$total_items, {
268 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
269 'total_amount' => $self->total_setup,
273 #could/should add up the different recurring frequencies on lines of their own
274 # but this will cover the 95% cases for now
275 if ( $self->total_recur > 0 ) {
276 push @$total_items, {
277 'total_item' => $self->mt('Total Recurring'),
278 'total_amount' => $self->total_recur,
284 =item enable_previous
288 sub enable_previous { 0 }
290 =item convert_cust_main
292 If this quotation already belongs to a customer, then returns that customer, as
293 an FS::cust_main object.
295 Otherwise, creates a new customer (FS::cust_main object and record, and
296 associated) based on this quotation's prospect, then orders this quotation's
297 packages as real packages for the customer.
299 If there is an error, returns an error message, otherwise, returns the
300 newly-created FS::cust_main object.
304 sub convert_cust_main {
307 my $cust_main = $self->cust_main;
308 return $cust_main if $cust_main; #already converted, don't again
310 my $oldAutoCommit = $FS::UID::AutoCommit;
311 local $FS::UID::AutoCommit = 0;
314 $cust_main = $self->prospect_main->convert_cust_main;
315 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
316 $dbh->rollback if $oldAutoCommit;
320 $self->prospectnum('');
321 $self->custnum( $cust_main->custnum );
322 my $error = $self->replace || $self->order;
324 $dbh->rollback if $oldAutoCommit;
328 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
336 This method is for use with quotations which are already associated with a customer.
338 Orders this quotation's packages as real packages for the customer.
340 If there is an error, returns an error message, otherwise returns false.
347 tie my %cust_pkg, 'Tie::RefHash',
348 map { FS::cust_pkg->new({ pkgpart => $_->pkgpart,
349 quantity => $_->quantity,
353 $self->quotation_pkg ;
355 $self->cust_main->order_pkgs( \%cust_pkg );
361 One-time charges, like FS::cust_main::charge()
365 #super false laziness w/cust_main::charge
368 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
369 my ( $pkg, $comment, $additional );
370 my ( $setuptax, $taxclass ); #internal taxes
371 my ( $taxproduct, $override ); #vendor (CCH) taxes
373 my $cust_pkg_ref = '';
374 my ( $bill_now, $invoice_terms ) = ( 0, '' );
376 if ( ref( $_[0] ) ) {
377 $amount = $_[0]->{amount};
378 $setup_cost = $_[0]->{setup_cost};
379 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
380 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
381 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
382 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
383 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
384 : '$'. sprintf("%.2f",$amount);
385 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
386 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
387 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
388 $additional = $_[0]->{additional} || [];
389 $taxproduct = $_[0]->{taxproductnum};
390 $override = { '' => $_[0]->{tax_override} };
391 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
392 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
393 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
394 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
400 $pkg = @_ ? shift : 'One-time charge';
401 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
403 $taxclass = @_ ? shift : '';
407 local $SIG{HUP} = 'IGNORE';
408 local $SIG{INT} = 'IGNORE';
409 local $SIG{QUIT} = 'IGNORE';
410 local $SIG{TERM} = 'IGNORE';
411 local $SIG{TSTP} = 'IGNORE';
412 local $SIG{PIPE} = 'IGNORE';
414 my $oldAutoCommit = $FS::UID::AutoCommit;
415 local $FS::UID::AutoCommit = 0;
418 my $part_pkg = new FS::part_pkg ( {
420 'comment' => $comment,
424 'classnum' => ( $classnum ? $classnum : '' ),
425 'setuptax' => $setuptax,
426 'taxclass' => $taxclass,
427 'taxproductnum' => $taxproduct,
428 'setup_cost' => $setup_cost,
431 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
432 ( 0 .. @$additional - 1 )
434 'additional_count' => scalar(@$additional),
435 'setup_fee' => $amount,
438 my $error = $part_pkg->insert( options => \%options,
439 tax_overrides => $override,
442 $dbh->rollback if $oldAutoCommit;
446 my $pkgpart = $part_pkg->pkgpart;
449 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
451 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
452 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
453 $error = $type_pkgs->insert;
455 $dbh->rollback if $oldAutoCommit;
460 #except for DIFF, eveything above is idential to cust_main version
461 #but below is our own thing pretty much (adding a quotation package instead
462 # of ordering a customer package, no "bill now")
464 my $quotation_pkg = new FS::quotation_pkg ( {
465 'quotationnum' => $self->quotationnum,
466 'pkgpart' => $pkgpart,
467 'quantity' => $quantity,
468 #'start_date' => $start_date,
469 #'no_auto' => $no_auto,
470 'locationnum'=> $locationnum,
473 $error = $quotation_pkg->insert;
475 $dbh->rollback if $oldAutoCommit;
477 #} elsif ( $cust_pkg_ref ) {
478 # ${$cust_pkg_ref} = $cust_pkg;
481 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
488 Disables this quotation (sets disabled to Y, which hides the quotation on
489 prospects and customers).
491 If there is an error, returns an error message, otherwise returns false.
497 $self->disabled('Y');
503 Enables this quotation.
505 If there is an error, returns an error message, otherwise returns false.
522 =item search_sql_where HASHREF
524 Class method which returns an SQL WHERE fragment to search for parameters
525 specified in HASHREF. Valid parameters are
531 List reference of start date, end date, as UNIX timestamps.
541 List reference of charged limits (exclusive).
545 List reference of charged limits (exclusive).
549 flag, return open invoices only
553 flag, return net invoices only
561 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
565 sub search_sql_where {
566 my($class, $param) = @_;
568 # warn "$me search_sql_where called with params: \n".
569 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
575 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
576 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
580 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
581 # push @search, "cust_main.refnum = $1";
585 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
586 push @search, "quotation.prospectnum = $1";
590 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
591 push @search, "cust_bill.custnum = $1";
595 if ( $param->{_date} ) {
596 my($beginning, $ending) = @{$param->{_date}};
598 push @search, "quotation._date >= $beginning",
599 "quotation._date < $ending";
603 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
604 push @search, "quotation.quotationnum >= $1";
606 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
607 push @search, "quotation.quotationnum <= $1";
611 # if ( $param->{charged} ) {
612 # my @charged = ref($param->{charged})
613 # ? @{ $param->{charged} }
614 # : ($param->{charged});
616 # push @search, map { s/^charged/cust_bill.charged/; $_; }
620 my $owed_sql = FS::cust_bill->owed_sql;
623 push @search, "quotation._date < ". (time-86400*$param->{'days'})
626 #agent virtualization
627 my $curuser = $FS::CurrentUser::CurrentUser;
628 #false laziness w/search/quotation.html
629 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
630 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
633 join(' AND ', @search );
643 L<FS::Record>, schema.html from the base documentation.