2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
8 use FS::UID qw( dbh myconnect );
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearch qsearchs );
14 use FS::quotation_pkg;
15 use FS::quotation_pkg_tax;
24 FS::quotation - Object methods for quotation records
30 $record = new FS::quotation \%hash;
31 $record = new FS::quotation { 'column' => 'value' };
33 $error = $record->insert;
35 $error = $new_record->replace($old_record);
37 $error = $record->delete;
39 $error = $record->check;
43 An FS::quotation object represents a quotation. FS::quotation inherits from
44 FS::Record. The following fields are currently supported:
74 projected date when the quotation will be closed
78 projected confidence (expressed as integer) that quotation will close
88 Creates a new quotation. To add the quotation to the database, see L<"insert">.
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to. You can ask the object for a copy with the I<hash> method.
95 sub table { 'quotation'; }
96 sub notice_name { 'Quotation'; }
97 sub template_conf { 'quotation_'; }
98 sub has_sections { 1; }
102 Adds this record to the database. If there is an error, returns the error,
103 otherwise returns false.
107 Delete this record from the database.
109 =item replace OLD_RECORD
111 Replaces the OLD_RECORD with this one in the database. If there is an error,
112 returns the error, otherwise returns false.
116 Checks all fields to make sure this is a valid quotation. If there is
117 an error, returns the error, otherwise returns false. Called by the insert
126 $self->ut_numbern('quotationnum')
127 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
128 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
129 || $self->ut_numbern('_date')
130 || $self->ut_enum('disabled', [ '', 'Y' ])
131 || $self->ut_numbern('usernum')
132 || $self->ut_numbern('close_date')
133 || $self->ut_numbern('confidence')
135 return $error if $error;
137 $self->_date(time) unless $self->_date;
139 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
141 return 'confidence must be an integer between 1 and 100'
142 if length($self->confidence) && (($self->confidence < 1) || ($self->confidence > 100));
144 return 'prospectnum or custnum must be specified'
145 if ! $self->prospectnum
159 sub cust_bill_pkg { #actually quotation_pkg objects
160 shift->quotation_pkg(@_);
169 sprintf('%.2f', $self->_total('setup') + $self->_total('setup_tax'));
172 =item total_recur [ FREQ ]
178 #=item total_recur [ FREQ ]
179 #my $freq = @_ ? shift : '';
180 sprintf('%.2f', $self->_total('recur') + $self->_total('recur_tax'));
184 my( $self, $method ) = @_;
187 $total += $_->$method() for $self->quotation_pkg;
188 sprintf('%.2f', $total);
194 my $opt = shift || {};
195 if ($opt and !ref($opt)) {
196 die ref($self). '->email called with positional parameters';
199 my $conf = $self->conf;
201 my $from = delete $opt->{from};
203 # this is where we set the From: address
204 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
205 || $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
206 $self->SUPER::email( {
217 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
220 #my $cust_main = $self->cust_main;
221 #my $name = $cust_main->name;
222 #my $name_short = $cust_main->name_short;
223 #my $invoice_number = $self->invnum;
224 #my $invoice_date = $self->_date_pretty;
229 =item cust_or_prosect
233 sub cust_or_prospect {
235 $self->custnum ? $self->cust_main : $self->prospect_main;
238 =item cust_or_prospect_label_link
240 HTML links to either the customer or prospect.
242 Returns a list consisting of two elements. The first is a text label for the
243 link, and the second is the URL.
247 sub cust_or_prospect_label_link {
248 my( $self, $p ) = @_;
250 if ( my $custnum = $self->custnum ) {
251 my $display_custnum = $self->cust_main->display_custnum;
252 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
254 : ';show=quotations';
256 emt("View this customer (#[_1])",$display_custnum) =>
257 "${p}view/cust_main.cgi?custnum=$custnum$target"
259 } elsif ( my $prospectnum = $self->prospectnum ) {
261 emt("View this prospect (#[_1])",$prospectnum) =>
262 "${p}view/prospect_main.html?$prospectnum"
270 sub _items_sections {
273 my $escape = $opt{escape}; # the only one we care about
275 my %show; # package frequency => 1 if there's anything to display
276 my %subtotals = (); # package frequency => subtotal
277 my $disable_total = 0;
278 foreach my $pkg ($self->quotation_pkg) {
280 my $part_pkg = $pkg->part_pkg;
282 my $recur_freq = $part_pkg->freq;
283 $show{$recur_freq} = 1 if $pkg->unitrecur > 0 or $pkg->recur_show_zero;
284 $show{0} = 1 if $pkg->unitsetup > 0 or $pkg->setup_show_zero;
285 ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax;
286 ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax;
288 #this is a shitty hack based on what's in part_pkg/ at the moment
289 # but its good enough for the 99% common case of preventing totals from
290 # displaying for prorate packages
292 if $part_pkg->plan =~ /^(prorate|torrus|agent$)/
293 || $part_pkg->option('recur_method') eq 'prorate'
294 || ( $part_pkg->option('sync_bill_date')
296 && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it
300 my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs };
303 my $no_recurring = 0;
304 foreach my $freq (keys %subtotals) {
306 #next if $subtotals{$freq} == 0;
307 next if !$show{$freq};
310 List::MoreUtils::first_index { $_ eq $freq } @pkg_freq_order;
312 if ( $freq eq '0' ) {
313 if ( scalar(keys(%subtotals)) == 1 ) {
314 # there are no recurring packages
316 $desc = $self->mt('Charges');
318 $desc = $self->mt('Setup Charges');
321 $desc = $self->mt('Recurring Charges') . ' - ' .
322 ucfirst($self->mt(FS::Misc->pkg_freqs->{$freq}))
326 'description' => &$escape($desc),
327 'sort_weight' => $weight,
329 'subtotal' => sprintf('%.2f',$subtotals{$freq}),
333 unless ( $disable_total || $no_recurring ) {
335 $total += $_ for values %subtotals;
337 'description' => 'First payment',
339 'category' => 'Total category', #required but what's it used for?
340 'subtotal' => sprintf('%.2f',$total)
344 return \@sections, [];
347 =item enable_previous
351 sub enable_previous { 0 }
353 =item convert_cust_main [ PARAMS ]
355 If this quotation already belongs to a customer, then returns that customer, as
356 an FS::cust_main object.
358 Otherwise, creates a new customer (FS::cust_main object and record, and
359 associated) based on this quotation's prospect, then orders this quotation's
360 packages as real packages for the customer.
362 If there is an error, returns an error message, otherwise, returns the
363 newly-created FS::cust_main object.
365 Accepts the same params as L</order>.
369 sub convert_cust_main {
371 my $params = shift || {};
373 my $cust_main = $self->cust_main;
374 return $cust_main if $cust_main; #already converted, don't again
376 my $oldAutoCommit = $FS::UID::AutoCommit;
377 local $FS::UID::AutoCommit = 0;
380 $cust_main = $self->prospect_main->convert_cust_main;
381 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
382 $dbh->rollback if $oldAutoCommit;
386 $self->prospectnum('');
387 $self->custnum( $cust_main->custnum );
388 my $error = $self->replace || $self->order(undef,$params);
390 $dbh->rollback if $oldAutoCommit;
394 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
400 =item order [ HASHREF ] [ PARAMS ]
402 This method is for use with quotations which are already associated with a customer.
404 Orders this quotation's packages as real packages for the customer.
406 If there is an error, returns an error message, otherwise returns false.
408 If HASHREF is passed, it will be filled with a hash mapping the
409 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
412 If PARAMS hashref is passed, the following params are accepted:
414 onhold - if true, suspends newly ordered packages
420 my $pkgnum_map = shift || {};
421 my $params = shift || {};
422 my $details_map = {};
424 tie my %all_cust_pkg, 'Tie::RefHash';
425 foreach my $quotation_pkg ($self->quotation_pkg) {
426 my $cust_pkg = FS::cust_pkg->new;
427 $pkgnum_map->{ $quotation_pkg->quotationpkgnum } = $cust_pkg;
429 # details will be copied below, after package is ordered
430 $details_map->{ $quotation_pkg->quotationpkgnum } = [
431 map { $_->copy_on_order ? $_->detail : () } $quotation_pkg->quotation_pkg_detail
434 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
435 $cust_pkg->set( $_, $quotation_pkg->get($_) );
438 # can now have two discounts each (setup and recur)
439 foreach my $pkg_discount ($quotation_pkg->quotation_pkg_discount) {
440 my $field = $pkg_discount->setuprecur . '_discountnum';
441 $cust_pkg->set($field, $pkg_discount->discountnum);
444 $all_cust_pkg{$cust_pkg} = []; # no services
447 local $SIG{HUP} = 'IGNORE';
448 local $SIG{INT} = 'IGNORE';
449 local $SIG{QUIT} = 'IGNORE';
450 local $SIG{TERM} = 'IGNORE';
451 local $SIG{TSTP} = 'IGNORE';
452 local $SIG{PIPE} = 'IGNORE';
454 my $oldAutoCommit = $FS::UID::AutoCommit;
455 local $FS::UID::AutoCommit = 0;
458 my $error = $self->cust_main->order_pkgs( \%all_cust_pkg );
461 # copy details (copy_on_order filtering handled above)
462 foreach my $quotationpkgnum (keys %$details_map) {
463 next unless @{$details_map->{$quotationpkgnum}};
464 $error = $pkgnum_map->{$quotationpkgnum}->set_cust_pkg_detail(
466 @{$details_map->{$quotationpkgnum}}
472 if ($$params{'onhold'}) {
473 foreach my $quotationpkgnum (keys %$pkgnum_map) {
475 $error = $pkgnum_map->{$quotationpkgnum}->suspend();
480 $dbh->rollback if $oldAutoCommit;
484 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
486 foreach my $quotationpkgnum (keys %$pkgnum_map) {
487 # convert the objects to just pkgnums
488 my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
489 $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
498 One-time charges, like FS::cust_main::charge()
502 #super false laziness w/cust_main::charge
505 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
506 my ( $pkg, $comment, $additional );
507 my ( $setuptax, $taxclass ); #internal taxes
508 my ( $taxproduct, $override ); #vendor (CCH) taxes
510 my $cust_pkg_ref = '';
511 my ( $bill_now, $invoice_terms ) = ( 0, '' );
513 if ( ref( $_[0] ) ) {
514 $amount = $_[0]->{amount};
515 $setup_cost = $_[0]->{setup_cost};
516 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
517 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
518 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
519 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
520 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
521 : '$'. sprintf("%.2f",$amount);
522 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
523 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
524 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
525 $additional = $_[0]->{additional} || [];
526 $taxproduct = $_[0]->{taxproductnum};
527 $override = { '' => $_[0]->{tax_override} };
528 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
529 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
530 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
531 $locationnum = $_[0]->{locationnum};
537 $pkg = @_ ? shift : 'One-time charge';
538 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
540 $taxclass = @_ ? shift : '';
544 local $SIG{HUP} = 'IGNORE';
545 local $SIG{INT} = 'IGNORE';
546 local $SIG{QUIT} = 'IGNORE';
547 local $SIG{TERM} = 'IGNORE';
548 local $SIG{TSTP} = 'IGNORE';
549 local $SIG{PIPE} = 'IGNORE';
551 my $oldAutoCommit = $FS::UID::AutoCommit;
552 local $FS::UID::AutoCommit = 0;
555 my $part_pkg = new FS::part_pkg ( {
557 'comment' => $comment,
561 'classnum' => ( $classnum ? $classnum : '' ),
562 'setuptax' => $setuptax,
563 'taxclass' => $taxclass,
564 'taxproductnum' => $taxproduct,
565 'setup_cost' => $setup_cost,
568 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
569 ( 0 .. @$additional - 1 )
571 'additional_count' => scalar(@$additional),
572 'setup_fee' => $amount,
575 my $error = $part_pkg->insert( options => \%options,
576 tax_overrides => $override,
579 $dbh->rollback if $oldAutoCommit;
583 my $pkgpart = $part_pkg->pkgpart;
586 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
588 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
589 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
590 $error = $type_pkgs->insert;
592 $dbh->rollback if $oldAutoCommit;
597 #except for DIFF, eveything above is idential to cust_main version
598 #but below is our own thing pretty much (adding a quotation package instead
599 # of ordering a customer package, no "bill now")
601 my $quotation_pkg = new FS::quotation_pkg ( {
602 'quotationnum' => $self->quotationnum,
603 'pkgpart' => $pkgpart,
604 'quantity' => $quantity,
605 #'start_date' => $start_date,
606 #'no_auto' => $no_auto,
607 'locationnum'=> $locationnum,
610 $error = $quotation_pkg->insert;
612 $dbh->rollback if $oldAutoCommit;
614 #} elsif ( $cust_pkg_ref ) {
615 # ${$cust_pkg_ref} = $cust_pkg;
618 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
625 Disables this quotation (sets disabled to Y, which hides the quotation on
626 prospects and customers).
628 If there is an error, returns an error message, otherwise returns false.
634 $self->disabled('Y');
640 Enables this quotation.
642 If there is an error, returns an error message, otherwise returns false.
654 Calculates current prices for all items on this quotation, including
655 discounts and taxes, and updates the quotation_pkg records accordingly.
661 my $conf = FS::Conf->new;
663 my %pkgnum_of; # quotationpkgnum => temporary pkgnum
665 my $me = "[quotation #".$self->quotationnum."]"; # for debug messages
667 my @return_bill = ([]);
670 ###### BEGIN TRANSACTION ######
674 my $temp_dbh = myconnect();
675 local $FS::UID::dbh = $temp_dbh;
676 local $FS::UID::AutoCommit = 0;
678 my $fake_self = FS::quotation->new({ $self->hash });
680 # if this is a prospect, make them into a customer for now
681 # XXX prospects currently can't have service locations
682 my $cust_or_prospect = $self->cust_or_prospect;
684 if ( $cust_or_prospect->isa('FS::prospect_main') ) {
685 $cust_main = $cust_or_prospect->convert_cust_main;
686 die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
687 $fake_self->set('prospectnum', '');
688 $fake_self->set('custnum', $cust_main->custnum);
690 $cust_main = $cust_or_prospect;
694 $error = $fake_self->order(\%pkgnum_of);
695 die "$error (simulating package order)\n" if $error;
697 my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
699 # simulate the first bill
702 'pkg_list' => \@new_pkgs,
703 'time' => time, # an option to adjust this?
704 'return_bill' => $return_bill[0],
705 'no_usage_reset' => 1,
707 $error = $cust_main->bill(%bill_opt);
708 die "$error (simulating initial billing)\n" if $error;
710 # pick dates for future bills
712 foreach (@new_pkgs) {
713 my $bill = $_->get('bill');
715 push @{ $next_bill_pkgs{$bill} ||= [] }, $_;
719 foreach my $next_bill (keys %next_bill_pkgs) {
720 $bill_opt{'time'} = $next_bill;
721 $bill_opt{'return_bill'} = $return_bill[$i] = [];
722 $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
723 $error = $cust_main->bill(%bill_opt);
724 die "$error (simulating recurring billing cycle $i)\n" if $error;
731 ###### END TRANSACTION ######
732 my %quotationpkgnum_of = reverse %pkgnum_of;
735 warn "pkgnums:\n".Dumper(\%pkgnum_of);
736 warn Dumper(\@return_bill);
739 # Careful: none of the foreign keys in here are correct outside the sandbox.
740 # We have a translation table for pkgnums; all others are total lies.
742 my %quotation_pkg; # quotationpkgnum => quotation_pkg
743 foreach my $qp ($self->quotation_pkg) {
744 $quotation_pkg{$qp->quotationpkgnum} = $qp;
745 $qp->set($_, 0) foreach qw(unitsetup unitrecur);
746 $qp->set('freq', '');
747 # flush old tax records
748 foreach ($qp->quotation_pkg_tax) {
750 return "$error (flushing tax records for pkgpart ".$qp->part_pkg->pkgpart.")"
755 my %quotation_pkg_tax; # quotationpkgnum => tax name => quotation_pkg_tax obj
756 my %quotation_pkg_discount; # quotationpkgnum => setuprecur => quotation_pkg_discount obj
758 for (my $i = 0; $i < scalar(@return_bill); $i++) {
759 my $this_bill = $return_bill[$i]->[0];
761 warn "$me billing cycle $i produced no invoice\n";
767 foreach my $cust_bill_pkg (@{ $this_bill->get('cust_bill_pkg') }) {
768 my $pkgnum = $cust_bill_pkg->pkgnum;
769 $cust_bill_pkg{ $cust_bill_pkg->billpkgnum } = $cust_bill_pkg;
771 # taxes/fees; come back to it
772 push @nonpkg_lines, $cust_bill_pkg;
775 my $quotationpkgnum = $quotationpkgnum_of{$pkgnum};
776 my $qp = $quotation_pkg{$quotationpkgnum};
778 # XXX supplemental packages could do this (they have separate pkgnums)
779 # handle that special case at some point
780 warn "$me simulated bill returned a package not on the quotation (pkgpart ".$cust_bill_pkg->pkgpart.")\n";
784 # then this is the first (setup) invoice
785 $qp->set('start_date', $cust_bill_pkg->sdate);
786 $qp->set('unitsetup', $qp->unitsetup + $cust_bill_pkg->unitsetup);
787 # pkgpart_override is a possibility
789 # recurring invoice (should be only one of these per package, though
790 # it may have multiple lineitems with the same pkgnum)
791 $qp->set('unitrecur', $qp->unitrecur + $cust_bill_pkg->unitrecur);
795 if ( $cust_bill_pkg->get('discounts') ) {
796 # discount records are generated as (setup, recur).
797 # well, not always, sometimes it's just (recur), but fixing this
798 # is horribly invasive.
799 my $discount = $cust_bill_pkg->get('discounts')->[0];
802 # find the quotation_pkg_discount record for this billing pass...
803 my $setuprecur = $i ? 'recur' : 'setup';
804 my $qpd = $quotation_pkg_discount{$quotationpkgnum}{$setuprecur}
805 ||= qsearchs('quotation_pkg_discount', {
806 'quotationpkgnum' => $quotationpkgnum,
807 'setuprecur' => $setuprecur,
810 if (!$qpd) { #can't happen
811 warn "$me simulated bill returned a $setuprecur discount but no discount is in effect.\n";
814 $qpd->set('amount', $discount->amount);
817 } # end of discount stuff
822 foreach my $cust_bill_pkg (@nonpkg_lines) {
824 my $itemdesc = $cust_bill_pkg->itemdesc;
826 if ($cust_bill_pkg->feepart) {
827 warn "$me simulated bill included a non-package fee (feepart ".
828 $cust_bill_pkg->feepart.")\n";
831 my $links = $cust_bill_pkg->get('cust_bill_pkg_tax_location') ||
832 $cust_bill_pkg->get('cust_bill_pkg_tax_rate_location') ||
834 # breadth-first unrolled recursion:
835 # take each tax link and any tax-on-tax descendants, and merge them
836 # into a single quotation_pkg_tax record for each pkgnum/taxname
838 while (my $tax_link = shift @$links) {
839 my $target = $cust_bill_pkg{ $tax_link->taxable_billpkgnum }
840 or die "$me unable to resolve tax link\n";
841 if ($target->pkgnum) {
842 my $quotationpkgnum = $quotationpkgnum_of{$target->pkgnum};
843 # create this if there isn't one yet
844 my $qpt = $quotation_pkg_tax{$quotationpkgnum}{$itemdesc} ||=
845 FS::quotation_pkg_tax->new({
846 quotationpkgnum => $quotationpkgnum,
847 itemdesc => $itemdesc,
851 if ( $i == 0 ) { # first invoice
852 $qpt->set('setup_amount', $qpt->setup_amount + $tax_link->amount);
853 } else { # subsequent invoices
854 # this isn't perfectly accurate, but that's why it's an estimate
855 $qpt->set('recur_amount', $qpt->recur_amount + $tax_link->amount);
856 $qpt->set('setup_amount', sprintf('%.2f', $qpt->setup_amount - $tax_link->amount));
857 $qpt->set('setup_amount', 0) if $qpt->setup_amount < 0;
859 } elsif ($target->feepart) {
860 # do nothing; we already warned for the fee itself
862 # tax on tax: the tax target is another tax item.
863 # since this is an estimate, I'm just going to assign it to the
864 # first of the underlying packages. (RT#5243 is why we can't have
866 my $sublinks = $target->cust_bill_pkg_tax_rate_location;
867 if ($sublinks and $sublinks->[0]) {
868 $tax_link->set('taxable_billpkgnum', $sublinks->[0]->taxable_billpkgnum);
869 push @$links, $tax_link; #try again
871 warn "$me unable to assign tax on tax; ignoring\n";
874 } # while my $tax_link
876 } # foreach my $cust_bill_pkg
878 foreach my $quotation_pkg (values %quotation_pkg) {
879 $error = $quotation_pkg->replace;
880 return "$error (recording estimate for ".$quotation_pkg->part_pkg->pkg.")"
883 foreach (values %quotation_pkg_discount) {
884 # { setup => one, recur => another }
885 foreach my $quotation_pkg_discount (values %$_) {
886 $error = $quotation_pkg_discount->replace;
887 return "$error (recording estimated discount)"
891 foreach my $quotation_pkg_tax (map { values %$_ } values %quotation_pkg_tax) {
892 $error = $quotation_pkg_tax->insert;
893 return "$error (recording estimated tax for ".$quotation_pkg_tax->itemdesc.")"
906 =item search_sql_where HASHREF
908 Class method which returns an SQL WHERE fragment to search for parameters
909 specified in HASHREF. Valid parameters are
915 List reference of start date, end date, as UNIX timestamps.
925 List reference of charged limits (exclusive).
929 List reference of charged limits (exclusive).
933 flag, return open invoices only
937 flag, return net invoices only
945 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
949 sub search_sql_where {
950 my($class, $param) = @_;
952 # warn "$me search_sql_where called with params: \n".
953 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
959 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
960 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
964 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
965 # push @search, "cust_main.refnum = $1";
969 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
970 push @search, "quotation.prospectnum = $1";
974 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
975 push @search, "cust_bill.custnum = $1";
979 if ( $param->{_date} ) {
980 my($beginning, $ending) = @{$param->{_date}};
982 push @search, "quotation._date >= $beginning",
983 "quotation._date < $ending";
987 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
988 push @search, "quotation.quotationnum >= $1";
990 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
991 push @search, "quotation.quotationnum <= $1";
995 # if ( $param->{charged} ) {
996 # my @charged = ref($param->{charged})
997 # ? @{ $param->{charged} }
998 # : ($param->{charged});
1000 # push @search, map { s/^charged/cust_bill.charged/; $_; }
1004 my $owed_sql = FS::cust_bill->owed_sql;
1007 push @search, "quotation._date < ". (time-86400*$param->{'days'})
1008 if $param->{'days'};
1010 #agent virtualization
1011 my $curuser = $FS::CurrentUser::CurrentUser;
1012 #false laziness w/search/quotation.html
1013 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
1014 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
1017 join(' AND ', @search );
1023 Return line item hashes for each package on this quotation.
1028 my ($self, %options) = @_;
1029 my $escape = $options{'escape_function'};
1030 my $locale = $self->cust_or_prospect->locale;
1032 my $preref = $options{'preref_callback'};
1034 my $section = $options{'section'};
1035 my $freq = $section->{'category'};
1036 my @pkgs = $self->quotation_pkg;
1038 die "_items_pkg called without section->{'category'}"
1039 unless defined $freq;
1041 my %tax_item; # taxname => hashref, will be aggregated AT DISPLAY TIME
1042 # like we should have done in the first place
1044 foreach my $quotation_pkg (@pkgs) {
1045 my $part_pkg = $quotation_pkg->part_pkg;
1046 my @details = $quotation_pkg->details;
1049 'pkgnum' => $quotation_pkg->quotationpkgnum,
1050 'description' => $quotation_pkg->desc($locale),
1051 'ext_description' => \@details,
1052 'quantity' => $quotation_pkg->quantity,
1056 $setuprecur = 'setup';
1057 if ($part_pkg->freq ne '0') {
1058 # indicate that it's a setup fee on a recur package (cust_bill does
1060 $this_item->{'description'} .= ' Setup';
1063 # recur for this frequency
1064 next if $freq ne $part_pkg->freq;
1065 $setuprecur = 'recur';
1068 $this_item->{'unit_amount'} = sprintf('%.2f',
1069 $quotation_pkg->get('unit'.$setuprecur));
1070 $this_item->{'amount'} = sprintf('%.2f', $this_item->{'unit_amount'}
1071 * $quotation_pkg->quantity);
1072 next if $this_item->{'amount'} == 0 and !(
1073 $setuprecur eq 'setup'
1074 ? $quotation_pkg->setup_show_zero
1075 : $quotation_pkg->recur_show_zero
1079 $this_item->{'preref_html'} = &$preref($quotation_pkg);
1082 push @items, $this_item;
1083 my $discount = $quotation_pkg->_item_discount(setuprecur => $setuprecur);
1085 $_ = &{$escape}($_) foreach @{ $discount->{ext_description} };
1086 push @items, $discount;
1089 # each quotation_pkg_tax has two amounts: the amount charged on the
1090 # setup invoice, and the amount on the recurring invoice.
1091 foreach my $qpt ($quotation_pkg->quotation_pkg_tax) {
1092 my $this_tax = $tax_item{$qpt->itemdesc} ||= {
1094 'description' => $qpt->itemdesc,
1095 'ext_description' => [],
1098 $this_tax->{'amount'} += $qpt->get($setuprecur.'_amount');
1100 } # foreach $quotation_pkg
1102 foreach my $taxname ( sort { $a cmp $b } keys (%tax_item) ) {
1103 my $this_tax = $tax_item{$taxname};
1104 $this_tax->{'amount'} = sprintf('%.2f', $this_tax->{'amount'});
1105 next if $this_tax->{'amount'} == 0;
1106 push @items, $this_tax;
1122 L<FS::Record>, schema.html from the base documentation.