summaryrefslogtreecommitdiff
path: root/FS/FS/detail_format.pm
blob: 46c6300ad951b358e21fb01c853767c0549f9561 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package FS::detail_format;

use strict;
use vars qw( $DEBUG );
use FS::Conf;
use FS::cdr;
use FS::cust_bill_pkg_detail;
use FS::L10N;
use Date::Language;
use Text::CSV_XS;

my $me = '[FS::detail_format]';

=head1 NAME

FS::detail_format - invoice detail formatter

=head1 DESCRIPTION

An FS::detail_format object is a converter to create invoice details 
(L<FS::cust_bill_pkg_detail>) from call detail records (L<FS::cdr>)
or other usage information.  FS::detail_format inherits from nothing.

Subclasses of FS::detail_format represent specific detail formats.

=head1 CLASS METHODS

=over 4

=item new FORMAT, OPTIONS

Returns a new detail formatter.  The FORMAT argument is the name of 
a subclass.

OPTIONS may contain:

- buffer: an arrayref to store details into.  This may avoid the need for a
large copy operation at the end of processing.  However, since summary
formats will produce nothing until the end of processing, C<finish> must be
called after all CDRs have been appended.

- inbound: a flag telling the formatter to format CDRs for display to the
receiving party, rather than the originator.  In this case, the
L<FS::cdr_termination> object will be fetched and its values used for
rated_price, rated_seconds, rated_minutes, and svcnum.  This can be changed
with the C<inbound> method.

- locale: a locale string to use for static text and date formats.  This is
optional.

- rounding: the number of decimal places to show in the amount column. This
is optional, and defaults to whatever's in the schema (which is 4).

=cut

sub new {
  my $class = shift;
  if ( $class eq 'FS::detail_format' ) {
    my $format = shift
      or die "$me format name required";
    $class = "FS::detail_format::$format"
      unless $format =~ /^FS::detail_format::/;
  }
  eval "use $class";
  die "$me error loading $class: $@" if $@;
  my %opt = @_;

  my $locale = $opt{'locale'} || '';
  my $conf = FS::Conf->new({ locale => $locale });
  $locale ||= $conf->config('locale') || 'en_US';

  my %locale_info = FS::Locales->locale_info($locale);
  my $language_name = $locale_info{'name'};

  my $self = { conf => FS::Conf->new({ locale => $locale }),
               csv  => Text::CSV_XS->new({ binary => 1 }),
               inbound  => ($opt{'inbound'} ? 1 : 0),
               rounding => $opt{'rounding'},
               buffer   => ($opt{'buffer'} || []),
               _lh      => FS::L10N->get_handle($locale),
               _dh      => eval { Date::Language->new($language_name) } ||
                           Date::Language->new()
             };
  bless $self, $class;
}

=back

=head1 METHODS

=over 4

=item inbound VALUE

Set/get the 'inbound' flag.

=cut

sub inbound {
  my $self = shift;
  $self->{inbound} = ($_[0] > 0) if (@_);
  $self->{inbound};
}

=item phonenum VALUE

Set/get the locally meaningful phone number.  This is used to tag call details
for presentation on certain kinds of invoices.

=cut

sub phonenum {
  my $self = shift;
  $self->{phonenum} = shift if @_;
  $self->{phonenum};
}

=item append CDRS

Takes any number of call detail records (as L<FS::cdr> objects),
formats them, and appends them to the internal buffer.

By default, this simply calls C<single_detail> on each CDR in the 
set.  Subclasses should override C<append> and maybe C<finish> if 
they do not produce detail lines from CDRs in a 1:1 fashion.

The 'billpkgnum', 'invnum', 'pkgnum', and 'phonenum' fields will 
be set later.

=cut

sub append {
  my $self = shift;
  foreach (@_) {
    push @{ $self->{buffer} }, $self->single_detail($_);
  }
}

=item details

Returns all invoice detail records in the buffer.  This will perform 
a C<finish> first.  Subclasses generally shouldn't override this.

=cut

sub details {
  my $self = shift;
  $self->finish;
  @{ $self->{buffer} }
}

=item finish

Ensures that all invoice details are generated given the CDRs that 
have been appended.  By default, this does nothing.

=cut

sub finish {}

=item header

Returns a header row for the format, as an L<FS::cust_bill_pkg_detail>
object.  By default this has 'format' = 'C', 'detail' = the value 
returned by C<header_detail>, and all other fields empty.

This is called after C<finish>, so it can use information from the CDRs.

=cut

sub header {
  my $self = shift;

  FS::cust_bill_pkg_detail->new(
    { 'format' => 'C', 'detail' => $self->header_detail }
  )
}

=item single_detail CDR

Takes a single CDR and returns an invoice detail to describe it.

By default, this maps the following fields from the CDR:

acctid            => acctid
rated_price       => amount
rated_classnum    => classnum
rated_seconds     => duration
rated_regionname  => regionname
accountcode       => accountcode
startdate         => startdate

If the formatter is in inbound mode, it will look up a C<cdr_termination>
record and use rated_price and rated_seconds from that, and acctid will be
set to null to avoid linking the CDR to the detail record for the inbound
leg of the call.

'phonenum' is set to the internal C<phonenum> value set on the formatter
object.

It then calls C<columns> on the CDR to obtain a list of detail
columns, formats them as a CSV string, and stores that in the 
'detail' field.

=cut

sub single_detail {
  my $self = shift;
  my $cdr = shift;

  my @columns = $self->columns($cdr);
  my $status = $self->csv->combine(@columns);
  die "$me error combining ".$self->csv->error_input."\n"
    if !$status;

  my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
  my $price = $object->rated_price if $object;
  $price = 0 if $cdr->freesidestatus eq 'no-charge';
  $price = sprintf('%.*f', $self->{'rounding'}, $price)
    if $self->{'rounding'} and length($price);

  FS::cust_bill_pkg_detail->new( {
      'acctid'      => ($self->{inbound} ? '' : $cdr->acctid),
      'amount'      => $price,
      'classnum'    => $cdr->rated_classnum,
      'duration'    => $object->rated_seconds,
      'regionname'  => $cdr->rated_regionname,
      'accountcode' => $cdr->accountcode,
      'startdate'   => $cdr->startdate,
      'format'      => 'C',
      'detail'      => $self->csv->string,
      'phonenum'    => $self->phonenum,
  });
}

=item columns CDR

Returns a list of CSV columns (to be shown on the invoice) for
the CDR.  This is the method most subclasses should override.

=cut

sub columns {
  my $self = shift;
  die "$me no columns method in ".ref($self);
}

=item header_detail

Returns the 'detail' field for the header row.  This should 
probably be a CSV string of column headers for the values returned
by C<columns>.

=cut

sub header_detail {
  my $self = shift;
  die "$me no header_detail method in ".ref($self);
}

# convenience methods for subclasses

sub conf { $_[0]->{conf} }

sub csv { $_[0]->{csv} }

sub date_format {
  my $self = shift;
  $self->{date_format} ||= ($self->conf->config('date_format') || '%m/%d/%Y');
}

sub money_char {
  my $self = shift;
  $self->{money_char} ||= ($self->conf->config('money_char') || '$');
}

# localization methods

sub time2str_local {
  my $self = shift;
  $self->{_dh}->time2str(@_);
}

# header strings are now localized in FS::TemplateItem_Mixin::detail

#imitate previous behavior for now

sub duration {
  my $self = shift;
  my $cdr = shift;
  my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
  my $sec = $object->rated_seconds if $object;
  $sec ||= 0;
  # termination objects now have rated_granularity.
  if ( $object->rated_granularity eq '0' ) {
    '1 call';
  }
  elsif ( $object->rated_granularity eq '60' ) {
    sprintf('%dm', ($sec + 59)/60);
  }
  else {
    sprintf('%dm %ds', $sec / 60, $sec % 60);
  }
}

sub price {
  my $self = shift;
  my $cdr = shift;
  my $object = $self->{inbound} ? $cdr->cdr_termination(1) : $cdr;
  my $price = $object->rated_price if $object;
  $price = '0.00' if $object->freesidestatus eq 'no-charge';
  $price = sprintf('%.*f', $self->{'rounding'}, $price)
    if $self->{'rounding'};
  if (length($price)) {
    $price = sprintf('%.*f', $self->{'rounding'}, $price)
      if $self->{'rounding'};
    return $self->money_char . $price;
  } else {
    return '';
  }
}

1;