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:
81 Creates a new quotation. To add the quotation to the database, see L<"insert">.
83 Note that this stores the hash reference, not a distinct copy of the hash it
84 points to. You can ask the object for a copy with the I<hash> method.
88 sub table { 'quotation'; }
89 sub notice_name { 'Quotation'; }
90 sub template_conf { 'quotation_'; }
91 sub has_sections { 1; }
95 Adds this record to the database. If there is an error, returns the error,
96 otherwise returns false.
100 Delete this record from the database.
102 =item replace OLD_RECORD
104 Replaces the OLD_RECORD with this one in the database. If there is an error,
105 returns the error, otherwise returns false.
109 Checks all fields to make sure this is a valid quotation. If there is
110 an error, returns the error, otherwise returns false. Called by the insert
119 $self->ut_numbern('quotationnum')
120 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
121 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
122 || $self->ut_numbern('_date')
123 || $self->ut_enum('disabled', [ '', 'Y' ])
124 || $self->ut_numbern('usernum')
126 return $error if $error;
128 $self->_date(time) unless $self->_date;
130 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
132 return 'prospectnum or custnum must be specified'
133 if ! $self->prospectnum
147 sub cust_bill_pkg { #actually quotation_pkg objects
148 shift->quotation_pkg(@_);
157 sprintf('%.2f', $self->_total('setup') + $self->_total('setup_tax'));
160 =item total_recur [ FREQ ]
166 #=item total_recur [ FREQ ]
167 #my $freq = @_ ? shift : '';
168 sprintf('%.2f', $self->_total('recur') + $self->_total('recur_tax'));
172 my( $self, $method ) = @_;
175 $total += $_->$method() for $self->quotation_pkg;
176 sprintf('%.2f', $total);
182 my $opt = shift || {};
183 if ($opt and !ref($opt)) {
184 die ref($self). '->email called with positional parameters';
187 my $conf = $self->conf;
189 my $from = delete $opt->{from};
191 # this is where we set the From: address
192 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
193 || $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
194 $self->SUPER::email( {
205 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
208 #my $cust_main = $self->cust_main;
209 #my $name = $cust_main->name;
210 #my $name_short = $cust_main->name_short;
211 #my $invoice_number = $self->invnum;
212 #my $invoice_date = $self->_date_pretty;
217 =item cust_or_prosect
221 sub cust_or_prospect {
223 $self->custnum ? $self->cust_main : $self->prospect_main;
226 =item cust_or_prospect_label_link
228 HTML links to either the customer or prospect.
230 Returns a list consisting of two elements. The first is a text label for the
231 link, and the second is the URL.
235 sub cust_or_prospect_label_link {
236 my( $self, $p ) = @_;
238 if ( my $custnum = $self->custnum ) {
239 my $display_custnum = $self->cust_main->display_custnum;
240 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
242 : ';show=quotations';
244 emt("View this customer (#[_1])",$display_custnum) =>
245 "${p}view/cust_main.cgi?custnum=$custnum$target"
247 } elsif ( my $prospectnum = $self->prospectnum ) {
249 emt("View this prospect (#[_1])",$prospectnum) =>
250 "${p}view/prospect_main.html?$prospectnum"
258 sub _items_sections {
261 my $escape = $opt{escape}; # the only one we care about
263 my %show; # package frequency => 1 if there's anything to display
264 my %subtotals = (); # package frequency => subtotal
265 my $disable_total = 0;
266 foreach my $pkg ($self->quotation_pkg) {
268 my $part_pkg = $pkg->part_pkg;
270 my $recur_freq = $part_pkg->freq;
271 $show{$recur_freq} = 1 if $pkg->unitrecur > 0;
272 $show{0} = 1 if $pkg->unitsetup > 0;
273 ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax;
274 ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax;
276 #this is a shitty hack based on what's in part_pkg/ at the moment
277 # but its good enough for the 99% common case of preventing totals from
278 # displaying for prorate packages
280 if $part_pkg->plan =~ /^(prorate|torrus|agent$)/
281 || $part_pkg->option('recur_method') eq 'prorate'
282 || ( $part_pkg->option('sync_bill_date')
284 && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it
288 my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs };
291 my $no_recurring = 0;
292 foreach my $freq (keys %subtotals) {
294 #next if $subtotals{$freq} == 0;
295 next if !$show{$freq};
298 List::MoreUtils::first_index { $_ eq $freq } @pkg_freq_order;
300 if ( $freq eq '0' ) {
301 if ( scalar(keys(%subtotals)) == 1 ) {
302 # there are no recurring packages
304 $desc = $self->mt('Charges');
306 $desc = $self->mt('Setup Charges');
309 $desc = $self->mt('Recurring Charges') . ' - ' .
310 ucfirst($self->mt(FS::Misc->pkg_freqs->{$freq}))
314 'description' => &$escape($desc),
315 'sort_weight' => $weight,
317 'subtotal' => sprintf('%.2f',$subtotals{$freq}),
321 unless ( $disable_total || $no_recurring ) {
323 $total += $_ for values %subtotals;
325 'description' => 'First payment',
327 'category' => 'Total category', #required but what's it used for?
328 'subtotal' => sprintf('%.2f',$total)
332 return \@sections, [];
335 =item enable_previous
339 sub enable_previous { 0 }
341 =item convert_cust_main
343 If this quotation already belongs to a customer, then returns that customer, as
344 an FS::cust_main object.
346 Otherwise, creates a new customer (FS::cust_main object and record, and
347 associated) based on this quotation's prospect, then orders this quotation's
348 packages as real packages for the customer.
350 If there is an error, returns an error message, otherwise, returns the
351 newly-created FS::cust_main object.
355 sub convert_cust_main {
358 my $cust_main = $self->cust_main;
359 return $cust_main if $cust_main; #already converted, don't again
361 my $oldAutoCommit = $FS::UID::AutoCommit;
362 local $FS::UID::AutoCommit = 0;
365 $cust_main = $self->prospect_main->convert_cust_main;
366 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
367 $dbh->rollback if $oldAutoCommit;
371 $self->prospectnum('');
372 $self->custnum( $cust_main->custnum );
373 my $error = $self->replace || $self->order;
375 $dbh->rollback if $oldAutoCommit;
379 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
385 =item order [ HASHREF ]
387 This method is for use with quotations which are already associated with a customer.
389 Orders this quotation's packages as real packages for the customer.
391 If there is an error, returns an error message, otherwise returns false.
393 If HASHREF is passed, it will be filled with a hash mapping the
394 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
401 my $pkgnum_map = shift || {};
402 my $details_map = {};
404 tie my %all_cust_pkg, 'Tie::RefHash';
405 foreach my $quotation_pkg ($self->quotation_pkg) {
406 my $cust_pkg = FS::cust_pkg->new;
407 $pkgnum_map->{ $quotation_pkg->quotationpkgnum } = $cust_pkg;
409 # details will be copied below, after package is ordered
410 $details_map->{ $quotation_pkg->quotationpkgnum } = [
411 map { $_->copy_on_order ? $_->detail : () } $quotation_pkg->quotation_pkg_detail
414 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
415 $cust_pkg->set( $_, $quotation_pkg->get($_) );
418 # can now have two discounts each (setup and recur)
419 foreach my $pkg_discount ($quotation_pkg->quotation_pkg_discount) {
420 my $field = $pkg_discount->setuprecur . '_discountnum';
421 $cust_pkg->set($field, $pkg_discount->discountnum);
424 $all_cust_pkg{$cust_pkg} = []; # no services
427 local $SIG{HUP} = 'IGNORE';
428 local $SIG{INT} = 'IGNORE';
429 local $SIG{QUIT} = 'IGNORE';
430 local $SIG{TERM} = 'IGNORE';
431 local $SIG{TSTP} = 'IGNORE';
432 local $SIG{PIPE} = 'IGNORE';
434 my $oldAutoCommit = $FS::UID::AutoCommit;
435 local $FS::UID::AutoCommit = 0;
438 my $error = $self->cust_main->order_pkgs( \%all_cust_pkg );
441 # copy details (copy_on_order filtering handled above)
442 foreach my $quotationpkgnum (keys %$details_map) {
443 next unless @{$details_map->{$quotationpkgnum}};
444 $error = $pkgnum_map->{$quotationpkgnum}->set_cust_pkg_detail(
446 @{$details_map->{$quotationpkgnum}}
452 foreach my $quotationpkgnum (keys %$pkgnum_map) {
453 # convert the objects to just pkgnums
454 my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
455 $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
459 $dbh->rollback if $oldAutoCommit;
463 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
470 One-time charges, like FS::cust_main::charge()
474 #super false laziness w/cust_main::charge
477 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
478 my ( $pkg, $comment, $additional );
479 my ( $setuptax, $taxclass ); #internal taxes
480 my ( $taxproduct, $override ); #vendor (CCH) taxes
482 my $cust_pkg_ref = '';
483 my ( $bill_now, $invoice_terms ) = ( 0, '' );
485 if ( ref( $_[0] ) ) {
486 $amount = $_[0]->{amount};
487 $setup_cost = $_[0]->{setup_cost};
488 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
489 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
490 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
491 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
492 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
493 : '$'. sprintf("%.2f",$amount);
494 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
495 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
496 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
497 $additional = $_[0]->{additional} || [];
498 $taxproduct = $_[0]->{taxproductnum};
499 $override = { '' => $_[0]->{tax_override} };
500 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
501 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
502 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
503 $locationnum = $_[0]->{locationnum};
509 $pkg = @_ ? shift : 'One-time charge';
510 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
512 $taxclass = @_ ? shift : '';
516 local $SIG{HUP} = 'IGNORE';
517 local $SIG{INT} = 'IGNORE';
518 local $SIG{QUIT} = 'IGNORE';
519 local $SIG{TERM} = 'IGNORE';
520 local $SIG{TSTP} = 'IGNORE';
521 local $SIG{PIPE} = 'IGNORE';
523 my $oldAutoCommit = $FS::UID::AutoCommit;
524 local $FS::UID::AutoCommit = 0;
527 my $part_pkg = new FS::part_pkg ( {
529 'comment' => $comment,
533 'classnum' => ( $classnum ? $classnum : '' ),
534 'setuptax' => $setuptax,
535 'taxclass' => $taxclass,
536 'taxproductnum' => $taxproduct,
537 'setup_cost' => $setup_cost,
540 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
541 ( 0 .. @$additional - 1 )
543 'additional_count' => scalar(@$additional),
544 'setup_fee' => $amount,
547 my $error = $part_pkg->insert( options => \%options,
548 tax_overrides => $override,
551 $dbh->rollback if $oldAutoCommit;
555 my $pkgpart = $part_pkg->pkgpart;
558 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
560 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
561 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
562 $error = $type_pkgs->insert;
564 $dbh->rollback if $oldAutoCommit;
569 #except for DIFF, eveything above is idential to cust_main version
570 #but below is our own thing pretty much (adding a quotation package instead
571 # of ordering a customer package, no "bill now")
573 my $quotation_pkg = new FS::quotation_pkg ( {
574 'quotationnum' => $self->quotationnum,
575 'pkgpart' => $pkgpart,
576 'quantity' => $quantity,
577 #'start_date' => $start_date,
578 #'no_auto' => $no_auto,
579 'locationnum'=> $locationnum,
582 $error = $quotation_pkg->insert;
584 $dbh->rollback if $oldAutoCommit;
586 #} elsif ( $cust_pkg_ref ) {
587 # ${$cust_pkg_ref} = $cust_pkg;
590 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
597 Disables this quotation (sets disabled to Y, which hides the quotation on
598 prospects and customers).
600 If there is an error, returns an error message, otherwise returns false.
606 $self->disabled('Y');
612 Enables this quotation.
614 If there is an error, returns an error message, otherwise returns false.
626 Calculates current prices for all items on this quotation, including
627 discounts and taxes, and updates the quotation_pkg records accordingly.
633 my $conf = FS::Conf->new;
635 my %pkgnum_of; # quotationpkgnum => temporary pkgnum
637 my $me = "[quotation #".$self->quotationnum."]"; # for debug messages
639 my @return_bill = ([]);
642 ###### BEGIN TRANSACTION ######
646 my $temp_dbh = myconnect();
647 local $FS::UID::dbh = $temp_dbh;
648 local $FS::UID::AutoCommit = 0;
650 my $fake_self = FS::quotation->new({ $self->hash });
652 # if this is a prospect, make them into a customer for now
653 # XXX prospects currently can't have service locations
654 my $cust_or_prospect = $self->cust_or_prospect;
656 if ( $cust_or_prospect->isa('FS::prospect_main') ) {
657 $cust_main = $cust_or_prospect->convert_cust_main;
658 die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
659 $fake_self->set('prospectnum', '');
660 $fake_self->set('custnum', $cust_main->custnum);
662 $cust_main = $cust_or_prospect;
666 $error = $fake_self->order(\%pkgnum_of);
667 die "$error (simulating package order)\n" if $error;
669 my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
671 # simulate the first bill
674 'pkg_list' => \@new_pkgs,
675 'time' => time, # an option to adjust this?
676 'return_bill' => $return_bill[0],
677 'no_usage_reset' => 1,
679 $error = $cust_main->bill(%bill_opt);
680 die "$error (simulating initial billing)\n" if $error;
682 # pick dates for future bills
684 foreach (@new_pkgs) {
685 my $bill = $_->get('bill');
687 push @{ $next_bill_pkgs{$bill} ||= [] }, $_;
691 foreach my $next_bill (keys %next_bill_pkgs) {
692 $bill_opt{'time'} = $next_bill;
693 $bill_opt{'return_bill'} = $return_bill[$i] = [];
694 $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
695 $error = $cust_main->bill(%bill_opt);
696 die "$error (simulating recurring billing cycle $i)\n" if $error;
703 ###### END TRANSACTION ######
704 my %quotationpkgnum_of = reverse %pkgnum_of;
707 warn "pkgnums:\n".Dumper(\%pkgnum_of);
708 warn Dumper(\@return_bill);
711 # Careful: none of the foreign keys in here are correct outside the sandbox.
712 # We have a translation table for pkgnums; all others are total lies.
714 my %quotation_pkg; # quotationpkgnum => quotation_pkg
715 foreach my $qp ($self->quotation_pkg) {
716 $quotation_pkg{$qp->quotationpkgnum} = $qp;
717 $qp->set($_, 0) foreach qw(unitsetup unitrecur);
718 $qp->set('freq', '');
719 # flush old tax records
720 foreach ($qp->quotation_pkg_tax) {
722 return "$error (flushing tax records for pkgpart ".$qp->part_pkg->pkgpart.")"
727 my %quotation_pkg_tax; # quotationpkgnum => tax name => quotation_pkg_tax obj
728 my %quotation_pkg_discount; # quotationpkgnum => setuprecur => quotation_pkg_discount obj
730 for (my $i = 0; $i < scalar(@return_bill); $i++) {
731 my $this_bill = $return_bill[$i]->[0];
733 warn "$me billing cycle $i produced no invoice\n";
739 foreach my $cust_bill_pkg (@{ $this_bill->get('cust_bill_pkg') }) {
740 my $pkgnum = $cust_bill_pkg->pkgnum;
741 $cust_bill_pkg{ $cust_bill_pkg->billpkgnum } = $cust_bill_pkg;
743 # taxes/fees; come back to it
744 push @nonpkg_lines, $cust_bill_pkg;
747 my $quotationpkgnum = $quotationpkgnum_of{$pkgnum};
748 my $qp = $quotation_pkg{$quotationpkgnum};
750 # XXX supplemental packages could do this (they have separate pkgnums)
751 # handle that special case at some point
752 warn "$me simulated bill returned a package not on the quotation (pkgpart ".$cust_bill_pkg->pkgpart.")\n";
756 # then this is the first (setup) invoice
757 $qp->set('start_date', $cust_bill_pkg->sdate);
758 $qp->set('unitsetup', $qp->unitsetup + $cust_bill_pkg->unitsetup);
759 # pkgpart_override is a possibility
761 # recurring invoice (should be only one of these per package, though
762 # it may have multiple lineitems with the same pkgnum)
763 $qp->set('unitrecur', $qp->unitrecur + $cust_bill_pkg->unitrecur);
767 if ( $cust_bill_pkg->get('discounts') ) {
768 # discount records are generated as (setup, recur).
769 # well, not always, sometimes it's just (recur), but fixing this
770 # is horribly invasive.
771 my $discount = $cust_bill_pkg->get('discounts')->[0];
774 # find the quotation_pkg_discount record for this billing pass...
775 my $setuprecur = $i ? 'recur' : 'setup';
776 my $qpd = $quotation_pkg_discount{$quotationpkgnum}{$setuprecur}
777 ||= qsearchs('quotation_pkg_discount', {
778 'quotationpkgnum' => $quotationpkgnum,
779 'setuprecur' => $setuprecur,
782 if (!$qpd) { #can't happen
783 warn "$me simulated bill returned a $setuprecur discount but no discount is in effect.\n";
786 $qpd->set('amount', $discount->amount);
789 } # end of discount stuff
794 foreach my $cust_bill_pkg (@nonpkg_lines) {
796 my $itemdesc = $cust_bill_pkg->itemdesc;
798 if ($cust_bill_pkg->feepart) {
799 warn "$me simulated bill included a non-package fee (feepart ".
800 $cust_bill_pkg->feepart.")\n";
803 my $links = $cust_bill_pkg->get('cust_bill_pkg_tax_location') ||
804 $cust_bill_pkg->get('cust_bill_pkg_tax_rate_location') ||
806 # breadth-first unrolled recursion:
807 # take each tax link and any tax-on-tax descendants, and merge them
808 # into a single quotation_pkg_tax record for each pkgnum/taxname
810 while (my $tax_link = shift @$links) {
811 my $target = $cust_bill_pkg{ $tax_link->taxable_billpkgnum }
812 or die "$me unable to resolve tax link\n";
813 if ($target->pkgnum) {
814 my $quotationpkgnum = $quotationpkgnum_of{$target->pkgnum};
815 # create this if there isn't one yet
816 my $qpt = $quotation_pkg_tax{$quotationpkgnum}{$itemdesc} ||=
817 FS::quotation_pkg_tax->new({
818 quotationpkgnum => $quotationpkgnum,
819 itemdesc => $itemdesc,
823 if ( $i == 0 ) { # first invoice
824 $qpt->set('setup_amount', $qpt->setup_amount + $tax_link->amount);
825 } else { # subsequent invoices
826 # this isn't perfectly accurate, but that's why it's an estimate
827 $qpt->set('recur_amount', $qpt->recur_amount + $tax_link->amount);
828 $qpt->set('setup_amount', sprintf('%.2f', $qpt->setup_amount - $tax_link->amount));
829 $qpt->set('setup_amount', 0) if $qpt->setup_amount < 0;
831 } elsif ($target->feepart) {
832 # do nothing; we already warned for the fee itself
834 # tax on tax: the tax target is another tax item.
835 # since this is an estimate, I'm just going to assign it to the
836 # first of the underlying packages. (RT#5243 is why we can't have
838 my $sublinks = $target->cust_bill_pkg_tax_rate_location;
839 if ($sublinks and $sublinks->[0]) {
840 $tax_link->set('taxable_billpkgnum', $sublinks->[0]->taxable_billpkgnum);
841 push @$links, $tax_link; #try again
843 warn "$me unable to assign tax on tax; ignoring\n";
846 } # while my $tax_link
848 } # foreach my $cust_bill_pkg
850 foreach my $quotation_pkg (values %quotation_pkg) {
851 $error = $quotation_pkg->replace;
852 return "$error (recording estimate for ".$quotation_pkg->part_pkg->pkg.")"
855 foreach (values %quotation_pkg_discount) {
856 # { setup => one, recur => another }
857 foreach my $quotation_pkg_discount (values %$_) {
858 $error = $quotation_pkg_discount->replace;
859 return "$error (recording estimated discount)"
863 foreach my $quotation_pkg_tax (map { values %$_ } values %quotation_pkg_tax) {
864 $error = $quotation_pkg_tax->insert;
865 return "$error (recording estimated tax for ".$quotation_pkg_tax->itemdesc.")"
878 =item search_sql_where HASHREF
880 Class method which returns an SQL WHERE fragment to search for parameters
881 specified in HASHREF. Valid parameters are
887 List reference of start date, end date, as UNIX timestamps.
897 List reference of charged limits (exclusive).
901 List reference of charged limits (exclusive).
905 flag, return open invoices only
909 flag, return net invoices only
917 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
921 sub search_sql_where {
922 my($class, $param) = @_;
924 # warn "$me search_sql_where called with params: \n".
925 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
931 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
932 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
936 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
937 # push @search, "cust_main.refnum = $1";
941 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
942 push @search, "quotation.prospectnum = $1";
946 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
947 push @search, "cust_bill.custnum = $1";
951 if ( $param->{_date} ) {
952 my($beginning, $ending) = @{$param->{_date}};
954 push @search, "quotation._date >= $beginning",
955 "quotation._date < $ending";
959 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
960 push @search, "quotation.quotationnum >= $1";
962 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
963 push @search, "quotation.quotationnum <= $1";
967 # if ( $param->{charged} ) {
968 # my @charged = ref($param->{charged})
969 # ? @{ $param->{charged} }
970 # : ($param->{charged});
972 # push @search, map { s/^charged/cust_bill.charged/; $_; }
976 my $owed_sql = FS::cust_bill->owed_sql;
979 push @search, "quotation._date < ". (time-86400*$param->{'days'})
982 #agent virtualization
983 my $curuser = $FS::CurrentUser::CurrentUser;
984 #false laziness w/search/quotation.html
985 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
986 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
989 join(' AND ', @search );
995 Return line item hashes for each package on this quotation.
1000 my ($self, %options) = @_;
1001 my $escape = $options{'escape_function'};
1002 my $locale = $self->cust_or_prospect->locale;
1004 my $preref = $options{'preref_callback'};
1006 my $section = $options{'section'};
1007 my $freq = $section->{'category'};
1008 my @pkgs = $self->quotation_pkg;
1010 die "_items_pkg called without section->{'category'}"
1011 unless defined $freq;
1013 my %tax_item; # taxname => hashref, will be aggregated AT DISPLAY TIME
1014 # like we should have done in the first place
1016 foreach my $quotation_pkg (@pkgs) {
1017 my $part_pkg = $quotation_pkg->part_pkg;
1018 my @details = $quotation_pkg->details;
1021 'pkgnum' => $quotation_pkg->quotationpkgnum,
1022 'description' => $quotation_pkg->desc($locale),
1023 'ext_description' => \@details,
1024 'quantity' => $quotation_pkg->quantity,
1028 $setuprecur = 'setup';
1029 if ($part_pkg->freq ne '0') {
1030 # indicate that it's a setup fee on a recur package (cust_bill does
1032 $this_item->{'description'} .= ' Setup';
1035 # recur for this frequency
1036 next if $freq ne $part_pkg->freq;
1037 $setuprecur = 'recur';
1040 $this_item->{'unit_amount'} = sprintf('%.2f',
1041 $quotation_pkg->get('unit'.$setuprecur));
1042 $this_item->{'amount'} = sprintf('%.2f', $this_item->{'unit_amount'}
1043 * $quotation_pkg->quantity);
1044 next if $this_item->{'amount'} == 0;
1047 $this_item->{'preref_html'} = &$preref($quotation_pkg);
1050 push @items, $this_item;
1051 my $discount = $quotation_pkg->_item_discount(setuprecur => $setuprecur);
1053 $_ = &{$escape}($_) foreach @{ $discount->{ext_description} };
1054 push @items, $discount;
1057 # each quotation_pkg_tax has two amounts: the amount charged on the
1058 # setup invoice, and the amount on the recurring invoice.
1059 foreach my $qpt ($quotation_pkg->quotation_pkg_tax) {
1060 my $this_tax = $tax_item{$qpt->itemdesc} ||= {
1062 'description' => $qpt->itemdesc,
1063 'ext_description' => [],
1066 $this_tax->{'amount'} += $qpt->get($setuprecur.'_amount');
1068 } # foreach $quotation_pkg
1070 foreach my $taxname ( sort { $a cmp $b } keys (%tax_item) ) {
1071 my $this_tax = $tax_item{$taxname};
1072 $this_tax->{'amount'} = sprintf('%.2f', $this_tax->{'amount'});
1073 next if $this_tax->{'amount'} == 0;
1074 push @items, $this_tax;
1090 L<FS::Record>, schema.html from the base documentation.