adjust IPifony download to show quantity/unit price in package desc, #18333
[freeside.git] / FS / bin / freeside-ipifony-download
1 #!/usr/bin/perl
2
3 use strict;
4 use Getopt::Std;
5 use Date::Format qw(time2str);
6 use File::Temp qw(tempdir);
7 use Net::SFTP::Foreign;
8 use FS::UID qw(adminsuidsetup);
9 use FS::Record qw(qsearch qsearchs);
10 use FS::cust_main;
11 use FS::Conf;
12 use File::Copy qw(copy);
13 use Text::CSV;
14
15 my %opt;
16 getopts('va:P:C:e:', \%opt);
17
18 # Product codes that are subject to flat rate E911 charges.  For these 
19 # products, the'quantity' field represents the number of lines.
20 my @E911_CODES = ( 'V-HPBX', 'V-TRUNK' );
21
22 # Map TAXNONVOICE/TAXVOICE to Freeside taxclass names
23 my %TAXCLASSES = (
24   'TAXNONVOICE' => 'Other',
25   'TAXVOICE'   => 'VoIP',
26 );
27   
28
29 #$Net::SFTP::Foreign::debug = -1;
30 sub HELP_MESSAGE { '
31   Usage:
32       freeside-ipifony-download 
33         [ -v ]
34         [ -a archivedir ]
35         [ -P port ]
36         [ -C category ]
37         [ -e pkgpart ]
38         freesideuser sftpuser@hostname[:path]
39 ' }
40
41 my @fields = (
42   'custnum',
43   'date_desc',
44   'quantity',
45   'unit_price',
46   'classname',
47   'taxclass',
48 );
49
50 my $user = shift or die &HELP_MESSAGE;
51 my $dbh = adminsuidsetup $user;
52 $FS::UID::AutoCommit = 0;
53
54 # for statistics
55 my $num_charges = 0;
56 my $num_errors = 0;
57 my $sum_charges = 0;
58 # cache classnums
59 my %classnum_of;
60
61 if ( $opt{a} ) {
62   die "no such directory: $opt{a}\n"
63     unless -d $opt{a};
64   die "archive directory $opt{a} is not writable by the freeside user\n"
65     unless -w $opt{a};
66 }
67
68 my $e911_part_pkg;
69 if ( $opt{e} ) {
70   $e911_part_pkg = FS::part_pkg->by_key($opt{e})
71     or die "E911 pkgpart $opt{e} not found.\n";
72
73   if ( $e911_part_pkg->base_recur > 0 or $e911_part_pkg->freq ) {
74     die "E911 pkgpart $opt{e} must be a one-time charge.\n";
75   }
76 }
77
78 my $categorynum = '';
79 if ( $opt{C} ) {
80   # find this category (don't auto-create it, it should exist already)
81   my $category = qsearchs('pkg_category', { categoryname => $opt{C} });
82   if (!defined($category)) {
83     die "Package category '$opt{C}' does not exist.\n";
84   }
85   $categorynum = $category->categorynum;
86 }
87
88 #my $tmpdir = File::Temp->newdir();
89 my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere?
90
91 my $host = shift
92   or die &HELP_MESSAGE;
93 my ($sftpuser, $path);
94 $host =~ s/^(.+)\@//;
95 $sftpuser = $1 || $ENV{USER};
96 $host =~ s/:(.*)//;
97 $path = $1;
98
99 my $port = 22;
100 if ( $opt{P} =~ /^(\d+)$/ ) {
101   $port = $1;
102 }
103
104 # for now assume SFTP download as the only method
105 print STDERR "Connecting to $sftpuser\@$host...\n" if $opt{v};
106
107 my $sftp = Net::SFTP::Foreign->new(
108   host      => $host,
109   user      => $sftpuser,
110   port      => $port,
111   # for now we don't support passwords. use authorized_keys.
112   timeout   => 30,
113   #more      => ($opt{v} ? '-v' : ''),
114 );
115 die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
116   if $sftp->error;
117
118 $sftp->setcwd($path) if $path;
119
120 my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
121 if (!@$files) {
122   print STDERR "No charge files found.\n" if $opt{v};
123   exit(-1);
124 }
125
126 my %cust_main; # cache
127 my %e911_qty; # custnum => sum of E911-subject quantity
128
129 my %is_e911 = map {$_ => 1} @E911_CODES;
130
131 FILE: foreach my $filename (@$files) {
132   print STDERR "Retrieving $filename\n" if $opt{v};
133   $sftp->get("ready/$filename", "$tmpdir/$filename");
134   if($sftp->error) {
135     warn "failed to download $filename\n";
136     next FILE;
137   }
138
139   # make sure server archive dir exists
140   if ( !$sftp->stat('done') ) {
141     print STDERR "Creating $path/done\n" if $opt{v};
142     $sftp->mkdir('done');
143     if($sftp->error) {
144       # something is seriously wrong
145       die "failed to create archive directory on server:\n".$sftp->error."\n";
146     }
147   }
148   #move to server archive dir
149   $sftp->rename("ready/$filename", "done/$filename");
150   if($sftp->error) {
151     warn "failed to archive $filename on server:\n".$sftp->error."\n";
152   } # process it anyway, I guess/
153
154   #copy to local archive dir
155   if ( $opt{a} ) {
156     print STDERR "Copying $tmpdir/$filename to archive dir $opt{a}\n"
157       if $opt{v};
158     copy("$tmpdir/$filename", $opt{a});
159     warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!;
160   }
161
162   open my $fh, "<$tmpdir/$filename";
163   my $csv = Text::CSV->new; # orthodox CSV
164   my %hash;
165   while (my $line = <$fh>) {
166     $csv->parse($line) or do {
167       warn "can't parse $filename: ".$csv->error_input."\n";
168       next FILE;
169     };
170     @hash{@fields} = $csv->fields();
171     if ( $hash{custnum} =~ /^cust/ ) {
172       # there appears to be a header row
173       print STDERR "skipping header row\n" if $opt{v};
174       next;
175     }
176     my $cust_main = 
177       $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
178     if (!$cust_main) {
179       warn "customer #$hash{custnum} not found\n";
180       next;
181     }
182     print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n"
183       if $opt{v};
184
185     my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
186     # construct arguments for $cust_main->charge
187     my %charge_opt = (
188       amount      => $amount,
189       quantity    => $hash{quantity},
190       start_date  => $cust_main->next_bill_date,
191       pkg         => $hash{date_desc} .
192                    ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)',
193       taxclass    => $TAXCLASSES{ $hash{taxclass} },
194     );
195     if (my $classname = $hash{classname}) {
196       if (!exists($classnum_of{$classname}) ) {
197         # then look it up
198         my $pkg_class = qsearchs('pkg_class', {
199             classname   => $classname,
200             categorynum => $categorynum,
201         });
202         if (!defined($pkg_class)) {
203           # then create it
204           $pkg_class = FS::pkg_class->new({
205               classname   => $classname,
206               categorynum => $categorynum,
207           });
208           my $error = $pkg_class->insert;
209           die "Error creating package class for product code '$classname':\n".
210             "$error\n"
211             if $error;
212         }
213
214         $classnum_of{$classname} = $pkg_class->classnum;
215       }
216       $charge_opt{classnum} = $classnum_of{$classname};
217     }
218     print STDERR "  Charging $hash{unit_price} * $hash{quantity}\n"
219       if $opt{v};
220     my $error = $cust_main->charge(\%charge_opt);
221     if ($error) {
222       warn "Error creating charge: $error" if $error;
223       $num_errors++;
224     } else {
225       $num_charges++;
226       $sum_charges += $hash{amount};
227     }
228
229     if ( $opt{e} and $is_e911{$hash{classname}} ) {
230       $e911_qty{$hash{custnum}} ||= 0;
231       $e911_qty{$hash{custnum}} += $hash{quantity};
232     }
233   } #while $line
234   close $fh;
235 } #FILE
236
237 # Order E911 packages
238 my $num_e911 = 0;
239 my $num_lines = 0;
240 foreach my $custnum ( keys (%e911_qty) ) {
241   my $cust_main = $cust_main{$custnum};
242   my $quantity = $e911_qty{$custnum};
243   next if $quantity == 0;
244   my $cust_pkg = FS::cust_pkg->new({
245       pkgpart     => $opt{e},
246       custnum     => $custnum,
247       start_date  => $cust_main->next_bill_date,
248       quantity    => $quantity,
249   });
250   my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg });
251   if ( $error ) {
252     warn "Error creating e911 charge for customer $custnum: $error\n";
253     $num_errors++;
254   } else {
255     $num_e911++;
256     $num_lines += $quantity;
257   }
258 }
259
260 $dbh->commit;
261
262 if ($opt{v}) {
263   print STDERR "
264 Finished!
265   Processed files: @$files
266   Created charges: $num_charges
267   Sum of charges: \$".sprintf('%0.2f', $sum_charges)."
268   E911 charges: $num_e911
269   E911 lines: $num_lines
270   Errors: $num_errors
271 ";
272 }
273
274 =head1 NAME
275
276 freeside-ipifony-download - Download and import invoice items from IPifony.
277
278 =head1 SYNOPSIS
279
280       freeside-ipifony-download 
281         [ -v ]
282         [ -a archivedir ]
283         [ -P port ]
284         [ -C category ]
285         [ -T taxclass ]
286         [ -e pkgpart ]
287         freesideuser sftpuser@hostname[:path]
288
289 =head1 REQUIRED PARAMETERS
290
291 I<freesideuser>: the Freeside user to run as.
292
293 I<sftpuser>: the SFTP user to connect as.  The 'freeside' system user should 
294 have an authorization key to connect as that user.
295
296 I<hostname>: the SFTP server.
297
298 =head1 OPTIONAL PARAMETERS
299
300 -v: Be verbose.
301
302 -a I<archivedir>: Save a copy of the downloaded file to I<archivedir>.
303
304 -P I<port>: Connect to that TCP port.
305
306 -C I<category>: The name of a package category to use when creating package
307 classes.
308
309 -e I<pkgpart>: The pkgpart (L<FS::part_pkg>) to use for E911 charges.  A 
310 package of this type will be ordered for each invoice that has E911-subject
311 line items.  The 'quantity' field on this package will be set to the total 
312 quantity of those line items.
313
314 The E911 package must be a one-time package (flat rate, no frequency, no 
315 recurring fee) with setup fee equal to the fee per line.
316
317 =cut
318
319 1;
320