stray closing /TABLE in the no-ticket case
[freeside.git] / FS / FS / TemplateItem_Mixin.pm
1 package FS::TemplateItem_Mixin;
2
3 use strict;
4 use vars qw( $DEBUG $me $conf $date_format );
5 use Carp;
6 use Date::Format;
7 use FS::UID;
8 use FS::Record qw( qsearch qsearchs dbh );
9 use FS::Conf;
10 use FS::part_pkg;
11 use FS::cust_pkg;
12
13 $DEBUG = 0;
14 $me = '[FS::TemplateItem_Mixin]';
15 FS::UID->install_callback( sub { 
16   $conf = new FS::Conf;
17   $date_format      = $conf->config('date_format')      || '%x'; #/YY
18 } );
19
20 =item cust_pkg
21
22 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
23
24 =cut
25
26 sub cust_pkg {
27   my $self = shift;
28   carp "$me $self -> cust_pkg" if $DEBUG;
29   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
30 }
31
32 =item part_pkg
33
34 Returns the package definition for this invoice line item.
35
36 =cut
37
38 sub part_pkg {
39   my $self = shift;
40   if ( $self->pkgpart_override ) {
41     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
42   } else {
43     my $part_pkg;
44     my $cust_pkg = $self->cust_pkg;
45     $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
46     $part_pkg;
47   }
48 }
49
50 =item part_fee
51
52 Returns the fee definition for this line item, if there is one.
53
54 =cut
55
56 sub part_fee {
57   my $self = shift;
58   $self->feepart
59     ? FS::part_fee->by_key($self->feepart)
60     : '';
61 }
62
63 =item part_X
64
65 Returns L</part_pkg> or L</part_fee>, whichever is applicable (or nothing,
66 if called on a tax line item).
67
68 =cut
69
70 sub part_X {
71   my $self = shift;
72   $self->part_pkg || $self->part_fee;
73 }
74
75 =item desc LOCALE
76
77 Returns a description for this line item.  For typical line items, this is the
78 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
79 For one-shot line items and named taxes, it is the I<itemdesc> field of this
80 line item, and for generic taxes, simply returns "Tax".
81
82 =cut
83
84 sub desc {
85   my( $self, $locale ) = @_;
86
87   if ( $self->pkgnum > 0 ) {
88     return $self->itemdesc if $self->itemdesc;
89     my $part_pkg = $self->part_pkg or return 'UNKNOWN';
90     return $part_pkg->pkg_locale($locale);
91
92   } elsif ( $self->feepart ) {
93     return $self->part_fee->itemdesc_locale($locale);
94
95   } else { # by the process of elimination it must be a tax
96     my $desc = $self->itemdesc || 'Tax';
97     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
98     return $desc;
99   }
100
101 }
102
103 =item time_period_pretty PART_PKG, AGENTNUM
104
105 Returns a formatted time period for this line item.
106
107 =cut
108
109 sub time_period_pretty {
110   my( $self, $part_pkg, $agentnum ) = @_;
111
112   #more efficient to look some of this conf stuff up outside the
113   # invoice/template display loop we're called from
114   # (Template_Mixin::_invoice_cust_bill_pkg) and pass them in as options
115
116   return '' if $conf->exists('disable_line_item_date_ranges')
117             || $part_pkg->option('disable_line_item_date_ranges',1)
118             || ! $self->sdate
119             || ! $self->edate;
120
121   my $date_style = '';
122   $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
123                                $agentnum
124                              )
125     if $part_pkg && $part_pkg->freq !~ /^1m?$/;
126   $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
127                                   $agentnum
128                                );
129
130   my $time_period;
131   if ( defined($date_style) && $date_style eq 'month_of' ) {
132     # (now watch, someone's going to make us do Chinese)
133     $time_period = $self->mt('The month of [_1]',
134                       $self->time2str_local('%B', $self->sdate)
135                    );
136   } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
137     my $desc = $conf->config( 'cust_bill-line_item-date_description',
138                                $agentnum
139                             );
140     $desc .= ' ' unless $desc =~ /\s$/;
141     $time_period = $desc. $self->time2str_local('%B', $self->sdate);
142   } else {
143     $time_period =      $self->time2str_local($date_format, $self->sdate).
144                  " - ". $self->time2str_local($date_format, $self->edate);
145   }
146
147   " ($time_period)";
148
149 }
150
151 =item details [ OPTION => VALUE ... ]
152
153 Returns an array of detail information for the invoice line item.
154
155 Options may include:
156
157 I<format>: set to 'html' or 'latex' to have the detail lines formatted for 
158 inclusion in an HTML table (wrapped in <tr> and <td> elements) or LaTeX table
159 (delimited with & and \\ operators).
160
161 I<escape_function>: if present, then the array elements are processed by this
162 function before being returned.
163
164 I<format_function>: overrides the normal HTML or LaTeX function for returning
165 formatted CDRs.
166
167 I<no_usage>: excludes call detail records.  The method will still return
168 some special-case records like prorate details, and manually created package 
169 details.
170
171 =cut
172
173 sub details {
174   my ( $self, %opt ) = @_;
175   my $escape_function = $opt{escape_function} || sub { shift };
176
177   my $csv = new Text::CSV_XS;
178   my $key = $self->primary_key;
179
180   if ( $opt{format_function} ) {
181
182     #this still expects to be passed a cust_bill_pkg_detail object as the
183     #second argument, which is expensive
184     carp "deprecated format_function passed to cust_bill_pkg->details";
185     my $format_sub = $opt{format_function} if $opt{format_function};
186
187     map { ( $_->format eq 'C'
188               ? &{$format_sub}( $_->detail, $_ )
189               : &{$escape_function}( $_->detail )
190           )
191         }
192       qsearch ({ 'table'    => $self->detail_table,
193                  'hashref'  => { $key => $self->get($key) },
194                  'order_by' => 'ORDER BY detailnum',
195               });
196
197   } elsif ( $opt{'no_usage'} ) {
198
199     my $sql = "SELECT detail FROM ". $self->detail_table.
200               "  WHERE " . $key . " = ". $self->get($key).
201               "    AND ( format IS NULL OR format != 'C' ) ".
202               "  ORDER BY detailnum";
203     my $sth = dbh->prepare($sql) or die dbh->errstr;
204     $sth->execute or die $sth->errstr;
205
206     map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
207
208   } else {
209
210     my $format_sub;
211     my $format = $opt{format} || '';
212     if ( $format eq 'html' ) {
213
214       $format_sub = sub { my $detail = shift;
215                           $csv->parse($detail) or return "can't parse $detail";
216                           join('</TD><TD>', map { &$escape_function($_) }
217                                             $csv->fields
218                               );
219                         };
220
221     } elsif ( $format eq 'latex' ) {
222
223       $format_sub = sub {
224         my $detail = shift;
225         $csv->parse($detail) or return "can't parse $detail";
226         #join(' & ', map { '\small{'. &$escape_function($_). '}' }
227         #            $csv->fields );
228         my $result = '';
229         my $column = 1;
230         foreach ($csv->fields) {
231           $result .= ' & ' if $column > 1;
232           if ($column > 6) {                     # KLUDGE ALERT!
233             $result .= '\multicolumn{1}{l}{\scriptsize{'.
234                        &$escape_function($_). '}}';
235           }else{
236             $result .= '\scriptsize{'.  &$escape_function($_). '}';
237           }
238           $column++;
239         }
240         $result;
241       };
242
243     } else {
244
245       $format_sub = sub { my $detail = shift;
246                           $csv->parse($detail) or return "can't parse $detail";
247                           join(' - ', map { &$escape_function($_) }
248                                       $csv->fields
249                               );
250                         };
251
252     }
253
254     my $sql = "SELECT format, detail FROM ". $self->detail_table.
255               "  WHERE " . $key . " = ". $self->get($key).
256               "  ORDER BY detailnum";
257     my $sth = dbh->prepare($sql) or die dbh->errstr;
258     $sth->execute or die $sth->errstr;
259
260     #avoid the fetchall_arrayref and loop for less memory usage?
261     # probably should use a cursor...
262
263     my @return;
264     my $head = 1;
265     map {
266       my $row = $_;
267       if (defined($row->[0]) and $row->[0] eq 'C') {
268         if ($head) {
269           # first CSV row = the format header; localize it but not the others
270           $row->[1] = $self->mt($row->[1]);
271           $head = 0;
272         }
273         &{$format_sub}($row->[1]);
274       } else {
275         &{$escape_function}($row->[1]);
276       }
277     } @{ $sth->fetchall_arrayref };
278
279   } #!$opt{format_function}
280
281 }
282
283 =item details_header [ OPTION => VALUE ... ]
284
285 Returns a list representing an invoice line item detail header, if any.
286 This relies on the behavior of voip_cdr in that it expects the header
287 to be the first CSV formatted detail (as is expected by invoice generation
288 routines).  Returns the empty list otherwise.
289
290 =cut
291
292 sub details_header {
293   my $self = shift;
294
295   my $csv = new Text::CSV_XS;
296
297   my @detail = 
298     qsearch ({ 'table'    => $self->detail_table,
299                'hashref'  => { 'billpkgnum' => $self->billpkgnum,
300                                'format'     => 'C',
301                              },
302                'order_by' => 'ORDER BY detailnum LIMIT 1',
303             });
304   return() unless scalar(@detail);
305   $csv->parse($detail[0]->detail) or return ();
306   $csv->fields;
307 }
308
309 =item quantity
310
311 =cut
312
313 sub quantity {
314   my( $self, $value ) = @_;
315   if ( defined($value) ) {
316     $self->setfield('quantity', $value);
317   }
318   $self->getfield('quantity') || 1;
319 }
320
321 =item unitsetup
322
323 =cut
324
325 sub unitsetup {
326   my( $self, $value ) = @_;
327   if ( defined($value) ) {
328     $self->setfield('unitsetup', $value);
329   }
330   $self->getfield('unitsetup') eq ''
331     ? $self->getfield('setup')
332     : $self->getfield('unitsetup');
333 }
334
335 =item unitrecur
336
337 =cut
338
339 sub unitrecur {
340   my( $self, $value ) = @_;
341   if ( defined($value) ) {
342     $self->setfield('unitrecur', $value);
343   }
344   $self->getfield('unitrecur') eq ''
345     ? $self->getfield('recur')
346     : $self->getfield('unitrecur');
347 }
348
349 =item cust_bill_pkg_display [ type => TYPE ]
350
351 Returns an array of display information for the invoice line item optionally
352 limited to 'TYPE'.
353
354 =cut
355
356 sub cust_bill_pkg_display {
357   my ( $self, %opt ) = @_;
358
359   my $class = 'FS::'. $self->display_table;
360
361   my $default = $class->new( { billpkgnum =>$self->billpkgnum } );
362
363   my $type = $opt{type} if exists $opt{type};
364   my @result;
365
366   if ( $self->get('display') ) {
367     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
368               @{ $self->get('display') };
369   } else {
370     my $hashref = { 'billpkgnum' => $self->billpkgnum };
371     $hashref->{type} = $type if defined($type);
372
373     my $order_by = $self->display_table_orderby || 'billpkgdisplaynum';
374     
375     @result = qsearch ({ 'table'    => $self->display_table,
376                          'hashref'  => $hashref,
377                          'order_by' => "ORDER BY $order_by",
378                       });
379   }
380
381   push @result, $default unless ( scalar(@result) || $type );
382
383   @result;
384
385 }
386
387 =item cust_bill_pkg_detail [ CLASSNUM ]
388
389 Returns the list of associated cust_bill_pkg_detail objects
390 The optional CLASSNUM argument will limit the details to the specified usage
391 class.
392
393 =cut
394
395 sub cust_bill_pkg_detail {
396   my $self = shift;
397   my $classnum = shift || '';
398
399   my %hash = ( 'billpkgnum' => $self->billpkgnum );
400   $hash{classnum} = $classnum if $classnum;
401
402   qsearch( $self->detail_table, \%hash ),
403
404 }
405
406 =item pkg_discount 
407
408 Returns the list of associated cust_bill_pkg_discount or 
409 quotation_pkg_discount objects.
410
411 =cut
412
413 sub pkg_discount {
414   my $self = shift;
415   my $pkey = $self->primary_key;
416   qsearch( $self->discount_table, { $pkey => $self->get($pkey) } );
417 }
418
419 1;