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