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
|
package FS::detail_format;
use strict;
use vars qw( $DEBUG );
use FS::Conf;
use FS::cdr;
use FS::cust_bill_pkg_detail;
use Date::Format qw(time2str);
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.
=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 $self = { conf => FS::Conf->new,
csv => Text::CSV_XS->new,
inbound => ($opt{'inbound'} ? 1 : 0),
buffer => ($opt{'buffer'} || []),
};
bless $self, $class;
}
=back
=head1 METHODS
=item inbound VALUE
Set/get the 'inbound' flag.
=cut
sub inbound {
my $self = shift;
$self->{inbound} = ($_[0] > 0) if (@_);
$self->{inbound};
}
=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:
rated_price => amount
rated_classnum => classnum
rated_seconds => duration
rated_regionname => regionname
accountcode => accountcode
startdate => startdate
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';
FS::cust_bill_pkg_detail->new( {
'amount' => $price,
'classnum' => $cdr->rated_classnum,
'duration' => $cdr->rated_seconds,
'regionname' => $cdr->rated_regionname,
'accountcode' => $cdr->accountcode,
'startdate' => $cdr->startdate,
'format' => 'C',
'detail' => $self->csv->string,
});
}
=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') || '$');
}
#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;
# XXX termination objects don't have rated_granularity so this may
# result in inbound CDRs being displayed as min/sec when they shouldn't.
# Should probably fix this.
if ( $cdr->rated_granularity eq '0' ) {
'1 call';
}
elsif ( $cdr->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';
length($price) ? $self->money_char . $price : '';
}
1;
|