avoid creating Set TimeWorked transactions where new value is empty, #28459
[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
5 use strict;
6 use FS::CurrentUser;
7
8 =head1 NAME
9
10 FS::quotation - Object methods for quotation records
11
12 =head1 SYNOPSIS
13
14   use FS::quotation;
15
16   $record = new FS::quotation \%hash;
17   $record = new FS::quotation { 'column' => 'value' };
18
19   $error = $record->insert;
20
21   $error = $new_record->replace($old_record);
22
23   $error = $record->delete;
24
25   $error = $record->check;
26
27 =head1 DESCRIPTION
28
29 An FS::quotation object represents a quotation.  FS::quotation inherits from
30 FS::Record.  The following fields are currently supported:
31
32 =over 4
33
34 =item quotationnum
35
36 primary key
37
38 =item prospectnum
39
40 prospectnum
41
42 =item custnum
43
44 custnum
45
46 =item _date
47
48 _date
49
50 =item disabled
51
52 disabled
53
54 =item usernum
55
56 usernum
57
58
59 =back
60
61 =head1 METHODS
62
63 =over 4
64
65 =item new HASHREF
66
67 Creates a new quotation.  To add the quotation to the database, see L<"insert">.
68
69 Note that this stores the hash reference, not a distinct copy of the hash it
70 points to.  You can ask the object for a copy with the I<hash> method.
71
72 =cut
73
74 sub table { 'quotation'; }
75 sub notice_name { 'Quotation'; }
76 sub template_conf { 'quotation_'; }
77
78 =item insert
79
80 Adds this record to the database.  If there is an error, returns the error,
81 otherwise returns false.
82
83 =item delete
84
85 Delete this record from the database.
86
87 =item replace OLD_RECORD
88
89 Replaces the OLD_RECORD with this one in the database.  If there is an error,
90 returns the error, otherwise returns false.
91
92 =item check
93
94 Checks all fields to make sure this is a valid quotation.  If there is
95 an error, returns the error, otherwise returns false.  Called by the insert
96 and replace methods.
97
98 =cut
99
100 sub check {
101   my $self = shift;
102
103   my $error = 
104     $self->ut_numbern('quotationnum')
105     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
106     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
107     || $self->ut_numbern('_date')
108     || $self->ut_enum('disabled', [ '', 'Y' ])
109     || $self->ut_numbern('usernum')
110   ;
111   return $error if $error;
112
113   $self->_date(time) unless $self->_date;
114
115   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
116
117   $self->SUPER::check;
118 }
119
120 =item prospect_main
121
122 =item cust_main
123
124 =item cust_bill_pkg
125
126 =cut
127
128 sub cust_bill_pkg { #actually quotation_pkg objects
129   shift->quotation_pkg(@_);
130 }
131
132 =item total_setup
133
134 =cut
135
136 sub total_setup {
137   my $self = shift;
138   $self->_total('setup');
139 }
140
141 =item total_recur [ FREQ ]
142
143 =cut
144
145 sub total_recur {
146   my $self = shift;
147 #=item total_recur [ FREQ ]
148   #my $freq = @_ ? shift : '';
149   $self->_total('recur');
150 }
151
152 sub _total {
153   my( $self, $method ) = @_;
154
155   my $total = 0;
156   $total += $_->$method() for $self->cust_bill_pkg;
157   sprintf('%.2f', $total);
158
159 }
160
161 #prevent things from falsely showing up as taxes, at least until we support
162 # quoting tax amounts..
163 sub _items_tax {
164   return ();
165 }
166 sub _items_nontax {
167   shift->cust_bill_pkg;
168 }
169
170 sub _items_total {
171   my( $self, $total_items ) = @_;
172
173   if ( $self->total_setup > 0 ) {
174     push @$total_items, {
175       'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
176       'total_amount' => $self->total_setup,
177     };
178   }
179
180   #could/should add up the different recurring frequencies on lines of their own
181   # but this will cover the 95% cases for now
182   if ( $self->total_recur > 0 ) {
183     push @$total_items, {
184       'total_item'   => $self->mt('Total Recurring'),
185       'total_amount' => $self->total_recur,
186     };
187   }
188
189 }
190
191 =item enable_previous
192
193 =cut
194
195 sub enable_previous { 0 }
196
197 =back
198
199 =head1 CLASS METHODS
200
201 =over 4
202
203
204 =item search_sql_where HASHREF
205
206 Class method which returns an SQL WHERE fragment to search for parameters
207 specified in HASHREF.  Valid parameters are
208
209 =over 4
210
211 =item _date
212
213 List reference of start date, end date, as UNIX timestamps.
214
215 =item invnum_min
216
217 =item invnum_max
218
219 =item agentnum
220
221 =item charged
222
223 List reference of charged limits (exclusive).
224
225 =item owed
226
227 List reference of charged limits (exclusive).
228
229 =item open
230
231 flag, return open invoices only
232
233 =item net
234
235 flag, return net invoices only
236
237 =item days
238
239 =item newest_percust
240
241 =back
242
243 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
244
245 =cut
246
247 sub search_sql_where {
248   my($class, $param) = @_;
249   #if ( $DEBUG ) {
250   #  warn "$me search_sql_where called with params: \n".
251   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
252   #}
253
254   my @search = ();
255
256   #agentnum
257   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
258     push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
259   }
260
261 #  #refnum
262 #  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
263 #    push @search, "cust_main.refnum = $1";
264 #  }
265
266   #prospectnum
267   if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
268     push @search, "quotation.prospectnum = $1";
269   }
270
271   #custnum
272   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
273     push @search, "cust_bill.custnum = $1";
274   }
275
276   #_date
277   if ( $param->{_date} ) {
278     my($beginning, $ending) = @{$param->{_date}};
279
280     push @search, "quotation._date >= $beginning",
281                   "quotation._date <  $ending";
282   }
283
284   #quotationnum
285   if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
286     push @search, "quotation.quotationnum >= $1";
287   }
288   if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
289     push @search, "quotation.quotationnum <= $1";
290   }
291
292 #  #charged
293 #  if ( $param->{charged} ) {
294 #    my @charged = ref($param->{charged})
295 #                    ? @{ $param->{charged} }
296 #                    : ($param->{charged});
297 #
298 #    push @search, map { s/^charged/cust_bill.charged/; $_; }
299 #                      @charged;
300 #  }
301
302   my $owed_sql = FS::cust_bill->owed_sql;
303
304   #days
305   push @search, "quotation._date < ". (time-86400*$param->{'days'})
306     if $param->{'days'};
307
308   #agent virtualization
309   my $curuser = $FS::CurrentUser::CurrentUser;
310   #false laziness w/search/quotation.html
311   push @search,' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
312                '   OR '. $curuser->agentnums_sql( table=>'cust_main' ).
313                ' )    ';
314
315   join(' AND ', @search );
316
317 }
318
319 =back
320
321 =head1 BUGS
322
323 =head1 SEE ALSO
324
325 L<FS::Record>, schema.html from the base documentation.
326
327 =cut
328
329 1;
330