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);
12 use File::Copy qw(copy);
16 getopts('vqa:P:C:e:', \%opt);
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' );
22 # Map TAXNONVOICE/TAXVOICE to Freeside taxclass names
24 'TAXNONVOICE' => 'Other',
29 #$Net::SFTP::Foreign::debug = -1;
32 freeside-ipifony-download
39 freesideuser sftpuser@hostname[:path]
51 my $user = shift or die &HELP_MESSAGE;
52 my $dbh = adminsuidsetup $user;
53 $FS::UID::AutoCommit = 0;
63 die "no such directory: $opt{a}\n"
65 die "archive directory $opt{a} is not writable by the freeside user\n"
71 $e911_part_pkg = FS::part_pkg->by_key($opt{e})
72 or die "E911 pkgpart $opt{e} not found.\n";
74 if ( $e911_part_pkg->base_recur > 0 or $e911_part_pkg->freq ) {
75 die "E911 pkgpart $opt{e} must be a one-time charge.\n";
81 # find this category (don't auto-create it, it should exist already)
82 my $category = qsearchs('pkg_category', { categoryname => $opt{C} });
83 if (!defined($category)) {
84 die "Package category '$opt{C}' does not exist.\n";
86 $categorynum = $category->categorynum;
89 #my $tmpdir = File::Temp->newdir();
90 my $tmpdir = tempdir( CLEANUP => 1 ); #DIR=>somewhere?
94 my ($sftpuser, $path);
96 $sftpuser = $1 || $ENV{USER};
101 if ( $opt{P} =~ /^(\d+)$/ ) {
105 # for now assume SFTP download as the only method
106 print STDERR "Connecting to $sftpuser\@$host...\n" if $opt{v};
108 my $sftp = Net::SFTP::Foreign->new(
112 # for now we don't support passwords. use authorized_keys.
114 #more => ($opt{v} ? '-v' : ''),
116 die "failed to connect to '$sftpuser\@$host'\n(".$sftp->error.")\n"
119 $sftp->setcwd($path) if $path;
121 my $files = $sftp->ls('ready', wanted => qr/\.csv$/, names_only => 1);
123 print STDERR "No charge files found.\n" if $opt{v};
127 my %cust_main; # cache
128 my %e911_qty; # custnum => sum of E911-subject quantity
130 my %is_e911 = map {$_ => 1} @E911_CODES;
132 FILE: foreach my $filename (@$files) {
133 print STDERR "Retrieving $filename\n" if $opt{v};
134 $sftp->get("ready/$filename", "$tmpdir/$filename");
136 warn "failed to download $filename\n";
140 # make sure server archive dir exists
141 if ( !$sftp->stat('done') ) {
142 print STDERR "Creating $path/done\n" if $opt{v};
143 $sftp->mkdir('done');
145 # something is seriously wrong
146 die "failed to create archive directory on server:\n".$sftp->error."\n";
149 #move to server archive dir
150 $sftp->rename("ready/$filename", "done/$filename");
152 warn "failed to archive $filename on server:\n".$sftp->error."\n";
153 } # process it anyway, I guess/
155 #copy to local archive dir
157 print STDERR "Copying $tmpdir/$filename to archive dir $opt{a}\n"
159 copy("$tmpdir/$filename", $opt{a});
160 warn "failed to copy $tmpdir/$filename to $opt{a}: $!" if $!;
163 open my $fh, "<$tmpdir/$filename";
164 my $csv = Text::CSV->new; # orthodox CSV
166 while (my $line = <$fh>) {
167 $csv->parse($line) or do {
168 warn "can't parse $filename: ".$csv->error_input."\n";
171 @hash{@fields} = $csv->fields();
172 if ( $hash{custnum} =~ /^cust/ ) {
173 # there appears to be a header row
174 print STDERR "skipping header row\n" if $opt{v};
178 $cust_main{$hash{custnum}} ||= FS::cust_main->by_key($hash{custnum});
180 warn "customer #$hash{custnum} not found\n";
183 print STDERR "Found customer #$hash{custnum}: ".$cust_main->name."\n"
186 my $amount = sprintf('%.2f',$hash{quantity} * $hash{unit_price});
188 # bill the charge on the customer's next bill date, if that's within
189 # the current calendar month; otherwise bill it immediately
191 my $next_bill_date = $cust_main->next_bill_date;
192 if ( $next_bill_date ) {
193 my ($bill_month, $bill_year) = (localtime($next_bill_date))[4, 5];
194 my ($this_month, $this_year) = (localtime(time))[4, 5];
195 if ( $this_month == $bill_month and $this_year == $bill_year ) {
196 $cust_main->set('charge_date', $next_bill_date);
200 # construct arguments for $cust_main->charge
202 amount => $hash{unit_price},
203 quantity => $hash{quantity},
204 start_date => $cust_main->get('charge_date'),
205 pkg => $hash{date_desc},
206 taxclass => $TAXCLASSES{ $hash{taxclass} },
209 $charge_opt{pkg} .= ' (' . $hash{quantity} . ' @ $' . $hash{unit_price} . ' ea)';
211 if (my $classname = $hash{classname}) {
212 if (!exists($classnum_of{$classname}) ) {
214 my $pkg_class = qsearchs('pkg_class', {
215 classname => $classname,
216 categorynum => $categorynum,
218 if (!defined($pkg_class)) {
220 $pkg_class = FS::pkg_class->new({
221 classname => $classname,
222 categorynum => $categorynum,
224 my $error = $pkg_class->insert;
225 die "Error creating package class for product code '$classname':\n".
230 $classnum_of{$classname} = $pkg_class->classnum;
232 $charge_opt{classnum} = $classnum_of{$classname};
234 print STDERR " Charging $hash{unit_price} * $hash{quantity}\n"
236 my $error = $cust_main->charge(\%charge_opt);
238 warn "Error creating charge: $error" if $error;
242 $sum_charges += $amount;
245 if ( $opt{e} and $is_e911{$hash{classname}} ) {
246 $e911_qty{$hash{custnum}} ||= 0;
247 $e911_qty{$hash{custnum}} += $hash{quantity};
253 # Order E911 packages
256 foreach my $custnum ( keys (%e911_qty) ) {
257 my $cust_main = $cust_main{$custnum};
258 my $quantity = $e911_qty{$custnum};
259 next if $quantity == 0;
260 my $cust_pkg = FS::cust_pkg->new({
263 start_date => $cust_main->get('charge_date'),
264 quantity => $quantity,
266 my $error = $cust_main->order_pkg({ cust_pkg => $cust_pkg });
268 warn "Error creating e911 charge for customer $custnum: $error\n";
272 $num_lines += $quantity;
281 Processed files: @$files
282 Created charges: $num_charges
283 Sum of charges: \$".sprintf('%0.2f', $sum_charges)."
284 E911 charges: $num_e911
285 E911 lines: $num_lines
292 freeside-ipifony-download - Download and import invoice items from IPifony.
296 freeside-ipifony-download
304 freesideuser sftpuser@hostname[:path]
306 =head1 REQUIRED PARAMETERS
308 I<freesideuser>: the Freeside user to run as.
310 I<sftpuser>: the SFTP user to connect as. The 'freeside' system user should
311 have an authorization key to connect as that user.
313 I<hostname>: the SFTP server.
315 =head1 OPTIONAL PARAMETERS
319 -q: Include the quantity and unit price in the charge description.
321 -a I<archivedir>: Save a copy of the downloaded file to I<archivedir>.
323 -P I<port>: Connect to that TCP port.
325 -C I<category>: The name of a package category to use when creating package
328 -e I<pkgpart>: The pkgpart (L<FS::part_pkg>) to use for E911 charges. A
329 package of this type will be ordered for each invoice that has E911-subject
330 line items. The 'quantity' field on this package will be set to the total
331 quantity of those line items.
333 The E911 package must be a one-time package (flat rate, no frequency, no
334 recurring fee) with setup fee equal to the fee per line.