Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 11 Oct 2012 22:15:48 +0000 (15:15 -0700)
committerIvan Kohler <ivan@freeside.biz>
Thu, 11 Oct 2012 22:15:48 +0000 (15:15 -0700)
23 files changed:
FS/FS/Misc.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/cdr.pm
FS/FS/cdr/qwest.pm [new file with mode: 0644]
FS/FS/cust_bill_ApplicationCommon.pm
FS/FS/msg_template.pm
FS/FS/part_event/Action/letter.pm [new file with mode: 0644]
FS/FS/part_export/acct_xmlrpc.pm
FS/FS/part_export/dma_radiusmanager.pm
FS/FS/part_tag.pm
FS/bin/freeside-wkhtmltopdf [new file with mode: 0755]
httemplate/edit/contact_class.html
httemplate/edit/cust_class.html
httemplate/edit/cust_main.cgi
httemplate/edit/cust_note_class.html
httemplate/edit/hardware_class.html
httemplate/edit/inventory_class.html
httemplate/edit/part_svc_class.html
httemplate/edit/part_tag.html
httemplate/edit/pkg_class.html
httemplate/edit/process/msg_template.html
httemplate/elements/tr-select-cust_tag.html

index 297e39f..a1c15fd 100644 (file)
@@ -799,7 +799,7 @@ sub _pslatex {
 
 }
 
-=item print ARRAYREF
+=item do_print ARRAYREF
 
 Sends the lines in ARRAYREF to the printer.
 
index 6ad4b74..fb1f1d6 100644 (file)
@@ -1349,6 +1349,7 @@ sub tables_hashref {
         'tagname',  'varchar',     '', $char_d, '', '',
         'tagdesc',  'varchar', 'NULL', $char_d, '', '',
         'tagcolor', 'varchar', 'NULL',       6, '', '',
+        'by_default',  'char', 'NULL',       1, '', '',
         'disabled',    'char', 'NULL',       1, '', '', 
       ],
       'primary_key' => 'tagnum',
index d35fd55..146e95f 100644 (file)
@@ -2251,7 +2251,8 @@ sub _items_cust_bill_pkg {
                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
 
-            if ( $cust_pkg->locationnum != $cust_main->ship_locationnum  ) {
+            if ( ! $cust_pkg->locationnum or
+                   $cust_pkg->locationnum != $cust_main->ship_locationnum  ) {
               my $loc = $cust_pkg->location_label;
               $loc = substr($loc, 0, $maxlength). '...'
                 if $format eq 'latex' && length($loc) > $maxlength;
index 05179f2..fdec921 100644 (file)
@@ -773,11 +773,16 @@ sub rate_prefix {
   my $seconds_left = $part_pkg->option_cacheable('use_duration')
                        ? $self->duration
                        : $self->billsec;
-  # charge for the first (conn_sec) seconds
-  my $seconds = min($seconds_left, $rate_detail->conn_sec);
-  $seconds_left -= $seconds; 
-  $weektime     += $seconds;
-  my $charge = $rate_detail->conn_charge; 
+
+  #no, do this later so it respects (group) included minutes
+  #  # charge for the first (conn_sec) seconds
+  #  my $seconds = min($seconds_left, $rate_detail->conn_sec);
+  #  $seconds_left -= $seconds; 
+  #  $weektime     += $seconds;
+  #  my $charge = $rate_detail->conn_charge; 
+  my $seconds = 0;
+  my $charge = 0;
+  my $connection_charged = 0;
 
   my $etime;
   while($seconds_left) {
@@ -840,6 +845,7 @@ sub rate_prefix {
 
     $seconds += $charge_sec;
 
+
     my $region_group = ($part_pkg->option_cacheable('min_included') || 0) > 0;
 
     ${$opt{region_group_included_min}} -= $minutes 
@@ -853,10 +859,21 @@ sub rate_prefix {
             )
        )
     {
+
+      #NOW do connection charges here... right?
+      #my $conn_seconds = min($seconds_left, $rate_detail->conn_sec);
+      my $conn_seconds = 0;
+      unless ( $connection_charged++ ) { #only one connection charge
+        $conn_seconds = min($charge_sec, $rate_detail->conn_sec);
+        $seconds_left -= $conn_seconds; 
+        $weektime     += $conn_seconds;
+        $charge += $rate_detail->conn_charge; 
+      }
+
                            #should preserve (display?) this
-      my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum};
+      my $charge_min = 0 - $included_min->{$regionnum}{$ratetimenum} - ( $conn_seconds / 60 );
       $included_min->{$regionnum}{$ratetimenum} = 0;
-      $charge += ($rate_detail->min_charge * $charge_min); #still not rounded
+      $charge += ($rate_detail->min_charge * $charge_min) if $charge_min > 0; #still not rounded
 
     } elsif ( ${$opt{region_group_included_min}} > 0
               && $region_group
diff --git a/FS/FS/cdr/qwest.pm b/FS/FS/cdr/qwest.pm
new file mode 100644 (file)
index 0000000..dd23858
--- /dev/null
@@ -0,0 +1,161 @@
+package FS::cdr::qwest;
+
+use strict;
+use vars qw(@ISA %info);
+use FS::cdr qw(_cdr_date_parser_maker);
+
+@ISA = qw(FS::cdr);
+
+my %disposition = (
+  0 => 'ANSWERED',  #normal completed call
+  1 => 'ANSWERED',  #"treated call"
+  2 => 'NO ANSWER', #abandoned call
+  3 => 'ERROR',     #abnormal call
+  4 => 'ERROR',     #signaling system error
+  5 => 'ANSWERED',  #forced disconnect
+  6 => 'ANSWERED',  #off-net route advance
+  7 => 'NO ANSWER', #test call
+  8 => 'NO ANSWER', #recorded promotion
+  9 => 'ERROR',     #TCAP DCP response time-out
+  12=> 'ANSWERED',  #abnormal release
+  13=> 'ERROR',     #"completed answer CDR"(?)
+  15=> 'ERROR',     #"COS failure"(?)
+);
+
+my $startdate = _cdr_date_parser_maker('startdate');
+my $enddate = _cdr_date_parser_maker('enddate');
+
+%info = (
+  'name'          => 'Qwest (Standard Daily)',
+  'weight'        => 400,
+  'type'          => 'fixedlength',
+  'fixedlength_format' => [qw(
+    billing_cycle_id:6:1:6
+    discn_dt:8:7:14
+    anstype:6:15:20
+    pindigs:4:21:24
+    origtime:6:25:30
+    discn_time:6:31:36
+    time_chng:1:37:37
+    ani:15:38:52
+    infodig:2:53:54
+    calldur:11:55:65
+    univacc:10:66:75
+    compcode:6:76:81
+    dialedno:15:82:96
+    calledno:15:97:111
+    predig:1:112:112
+    seqnum:11:113:123
+    orig_dt:8:124:131
+    finsid:6:132:137
+    trtmtcd:6:138:143
+    anisuff:6:144:149
+    origgrp:6:150:155
+    origmem:6:156:161
+    termgrp:6:162:167
+    termmem:6:168:173
+    fintkgrp:6:174:179
+    billnum:24:180:203
+    acctcd:12:204:215
+    swid:6:216:221
+    orig_bill_file_id:11:222:232
+    orig_trunk_group_name:12:233:244
+    orig_trunk_time_bias_ind:6:245:250
+    term_trunk_group_name:12:251:262
+    final_trunk_group_name:12:263:274
+    orig_trunk_usage_ind:6:275:280
+    orig_pricing_npa:3:281:283
+    orig_pricing_nxx:3:284:286
+    term_pricing_npa:3:287:289
+    term_pricing_nxx:3:290:292
+    prcmp_id:6:293:298
+    component_group_cd:2:299:300
+    component_group_val:24:301:324
+    intra_lata_ind:1:325:325
+    carrsel:1:326:326
+    cic:6:327:332
+    origlrn:10:333:342
+    portedno:10:343:352
+    lnpcheck:1:353:353
+  )],
+  'import_fields' => [
+    '',                 # billing_cycle_id
+    sub {               # discn_dt
+      # hold onto this, combine it with discn_time later
+      # YYYYMMDD
+      my ($cdr, $data, $conf, $param) = @_;
+      $param->{'discn_dt'} = $data;
+      '';
+    },
+    '',                 # anstype
+    '',                 # pindigs
+    sub {               # orig_time
+      # and this
+      # hhmmss
+      my ($cdr, $data, $conf, $param) = @_;
+      $param->{'orig_time'} = $data;
+      '';
+    },
+    sub {               # discn_time
+      my ($cdr, $data, $conf, $param) = @_;
+      $data = $param->{'discn_dt'} . $data; #YYYYMMDDhhmmss
+      $enddate->($cdr, $data);
+    },
+    '',                 # time_chng
+    'src',              # ani (originating number)
+    '',                 # infodig
+    'billsec',          # calldur
+    '',                 # univacc
+    sub {               # compcode
+      my ($cdr, $data) = @_;
+      my $compcode = sprintf('%d', $data);
+      $cdr->disposition($disposition{$compcode});
+      # only those that map to ANSWERED are billable, but that should be 
+      # set in rating options, not enforced here
+      '';
+    },
+    'dst',              # dialedno
+    '',                 # calledno (physical terminating number)
+    '',                 # predig (0/1/011 prefix)
+    '',                 # seqnum
+    sub {               # orig_dt
+      # backward from the discn_ fields
+      my ($cdr, $data, $conf, $param) = @_;
+      $data .= $param->{'orig_time'};
+      $startdate->($cdr, $data);
+    },
+    '',                 # finsid
+    '',                 # trtmtcd
+    '',                 # anisuff
+    'channel',          # origgrp (orig. trunk group)
+    '',                 # origmem (belongs in channel?)
+    'dstchannel',       # termgrp (term. trunk group)
+    '',                 # termmem (same?)
+    '',                 # fintkgrp
+    'charged_party',    # billnum (empty for "normal" calls)
+    '',                 # acctcd
+    '',                 # swid
+    '',                 # orig_bill_file_id
+    '',                 # orig_trunk_group_name
+    '',                 # orig_trunk_time_bias_ind
+    '',                 # term_trunk_group_name
+    '',                 # final_trunk_group_name
+    '',                 # orig_trunk_usage_ind
+    '',                 # orig_pricing_npa
+    '',                 # orig_pricing_nxx
+    '',                 # term_pricing_npa
+    '',                 # term_pricing_nxx
+    '',                 # prcmp_id
+    '',                 # component_group_cd
+    '',                 # component_group_val
+    '',                 # intra_lata_ind (or should we use this?)
+    '',                 # carrsel
+    '',                 # cic
+    '',                 # origlrn
+    '',                 # portedno
+    '',                 # lnpcheck
+  ],
+
+);
+
+1;
index cb07050..d8ccdd0 100644 (file)
@@ -1,9 +1,11 @@
 package FS::cust_bill_ApplicationCommon;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $skip_apply_to_lineitems_hack );
+use vars qw( @ISA $DEBUG $me $skip_apply_to_lineitems_hack $date_format );
 use List::Util qw(min);
+use Date::Format;
 use FS::Schema qw( dbdef );
+use FS::UID;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
 use FS::cust_svc;
@@ -18,6 +20,11 @@ $me = '[FS::cust_bill_ApplicationCommon]';
 
 $skip_apply_to_lineitems_hack = 0;
 
+FS::UID->install_callback( sub { 
+  my $conf = new FS::Conf;
+  $date_format = $conf->config('date_format') || '%x'; #/YY
+} );
+
 =head1 NAME
 
 FS::cust_bill_ApplicationCommon - Base class for bill application classes
@@ -500,7 +507,34 @@ Returns a string representing the invoice (see L<FS::cust_bill>), for example:
 
 sub applied_to_invoice {
   my $self = shift;
-  'applied to '. $self->cust_bill->invnum_date_pretty;
+  my $string = 'applied to '. $self->cust_bill->invnum_date_pretty;
+
+  #show application date if over 24 hours after (or before) payment/credit date
+  $string .= ' on '. $self->_date_pretty
+    if abs( $self->_date - $self->_app_source_object->_date ) > 86400;
+
+  $string;
+}
+
+=item _app_source_object 
+
+=cut
+
+sub _app_source_object {
+  my $self = shift;
+  my $source_table = $self->_app_source_table;
+  $self->$source_table();
+}
+
+=item _date_pretty
+
+Returns a string with the application date, for example: "3/20/2008"
+
+=cut
+
+sub _date_pretty {
+  my $self = shift;
+  time2str($date_format, $self->_date);
 }
 
 =item lineitem_breakdown_table 
index cac7fe5..e38346a 100644 (file)
@@ -16,6 +16,9 @@ use Date::Format qw( time2str );
 use HTML::Entities qw( decode_entities encode_entities ) ;
 use HTML::FormatText;
 use HTML::TreeBuilder;
+
+use File::Temp;
+use IPC::Run qw(run);
 use vars qw( $DEBUG $conf );
 
 FS::UID->install_callback( sub { $conf = new FS::Conf; } );
@@ -273,8 +276,8 @@ A hash reference of additional substitutions
 sub prepare {
   my( $self, %opt ) = @_;
 
-  my $cust_main = $opt{'cust_main'};
-  my $object = $opt{'object'};
+  my $cust_main = $opt{'cust_main'} or die 'cust_main required';
+  my $object = $opt{'object'} or die 'object required';
 
   # localization
   my $locale = $cust_main->locale || '';
@@ -435,9 +438,65 @@ sub send {
   send_email(generate_email($self->prepare(@_)));
 }
 
+=item render OPTION => VALUE ...
+
+Fills in the template and renders it to a PDF document.  Returns the 
+name of the PDF file.
+
+Options are as for 'prepare', but 'from' and 'to' are meaningless.
+
+=cut
+
+# will also have options to set paper size, margins, etc.
+
+sub render {
+  my $self = shift;
+  eval "use PDF::WebKit";
+  die $@ if $@;
+  my %opt = @_;
+  my %hash = $self->prepare(%opt);
+  my $html = $hash{'html_body'};
+
+  my $tmp = 'msg'.$self->msgnum.'-'.time2str('%Y%m%d', time).'-XXXXXXXX';
+  my $dir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc";
+
+  # Graphics/stylesheets should probably go in /var/www on the Freeside 
+  # machine.
+  my $kit = PDF::WebKit->new(\$html); #%options
+  # hack to use our wrapper script
+  $kit->configure(sub { shift->wkhtmltopdf('freeside-wkhtmltopdf') });
+  my $fh = File::Temp->new(
+    TEMPLATE  => $tmp,
+    DIR       => $dir,
+    UNLINK    => 0,
+    SUFFIX    => '.pdf'
+  );
+
+  print $fh $kit->to_pdf;
+  close $fh;
+  return $fh->filename;
+}
+
+=item print OPTIONS
+
+Render a PDF and send it to the printer.  OPTIONS are as for 'render'.
+
+=cut
+
+sub print {
+  my $file = render(@_);
+  my @lpr = $conf->config('lpr');
+  run ([@lpr, '-r'], '<', $file)
+    or die "lpr error:\n$?\n";
+}
+
+
 # helper sub for package dates
 my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' };
 
+# helper sub for money amounts
+my $money = sub { ($conf->money_char || '$') . sprintf('%.2f', $_[0] || 0) };
+
 # helper sub for usage-related messages
 my $usage_warning = sub {
   my $svc = shift;
@@ -483,6 +542,7 @@ sub substitutions {
       signupdate dundate
       packages recurdates
       ),
+      [ invoicing_email => sub { shift->invoicing_list_emailonly_scalar } ],
       #compatibility: obsolete ship_ fields - use the non-ship versions
       map (
         { my $field = $_;
@@ -520,6 +580,8 @@ sub substitutions {
       labels_short
       ),
       [ pkg               => sub { shift->part_pkg->pkg } ],
+      [ pkg_category      => sub { shift->part_pkg->categoryname } ],
+      [ pkg_class         => sub { shift->part_pkg->classname } ],
       [ cancel            => sub { shift->getfield('cancel') } ], # grrr...
       [ start_ymd         => sub { $ymd->(shift->getfield('start_date')) } ],
       [ setup_ymd         => sub { $ymd->(shift->getfield('setup')) } ],
@@ -529,6 +591,13 @@ sub substitutions {
       [ susp_ymd          => sub { $ymd->(shift->getfield('susp')) } ],
       [ expire_ymd        => sub { $ymd->(shift->getfield('expire')) } ],
       [ cancel_ymd        => sub { $ymd->(shift->getfield('cancel')) } ],
+
+      # not necessarily correct for non-flat packages
+      [ setup_fee         => sub { shift->part_pkg->option('setup_fee') } ],
+      [ recur_fee         => sub { shift->part_pkg->option('recur_fee') } ],
+
+      [ freq_pretty       => sub { shift->part_pkg->freq_pretty } ],
+
     ],
     'cust_bill' => [qw(
       invnum
diff --git a/FS/FS/part_event/Action/letter.pm b/FS/FS/part_event/Action/letter.pm
new file mode 100644 (file)
index 0000000..57b7b77
--- /dev/null
@@ -0,0 +1,47 @@
+package FS::part_event::Action::letter;
+
+use strict;
+use base qw( FS::part_event::Action );
+use FS::Record qw( qsearchs );
+use FS::msg_template;
+
+sub description { 'Print a form letter to the customer' }
+
+#sub eventtable_hashref {
+#    { 'cust_main' => 1,
+#      'cust_bill' => 1,
+#      'cust_pkg'  => 1,
+#    };
+#}
+
+sub option_fields {
+  (
+    'msgnum' => { 'label'    => 'Template',
+                  'type'     => 'select-table',
+                  'table'    => 'msg_template',
+                  'name_col' => 'msgname',
+                  'disable_empty' => 1,
+                },
+  );
+}
+
+sub default_weight { 56; } #?
+
+sub do_action {
+  my( $self, $object ) = @_;
+
+  my $cust_main = $self->cust_main($object);
+
+  my $msgnum = $self->option('msgnum');
+
+  my $msg_template = qsearchs('msg_template', { 'msgnum' => $msgnum } )
+      or die "Template $msgnum not found";
+
+  $msg_template->print(
+    'cust_main' => $cust_main,
+    'object'    => $object,
+  );
+
+}
+
+1;
index 3070f28..4c896b4 100644 (file)
@@ -5,6 +5,7 @@ use vars qw( %info ); # $DEBUG );
 #use Data::Dumper;
 use Tie::IxHash;
 use Frontier::Client; #to avoid adding a dependency on RPC::XML just now
+use Frontier::RPC2;
 #use FS::Record qw( qsearch qsearchs );
 use FS::Schema qw( dbdef );
 
@@ -189,18 +190,18 @@ sub _export_value {
   if ( $fields{$value} ) {
     my $type = dbdef->table('svc_acct')->column($value)->type;
     if ( $type =~ /^(int|serial)/i ) {
-      return Frontier::Client->new->int( $svc_acct->$value() );
+      return Frontier::RPC2::Integer->new( $svc_acct->$value() );
     } elsif ( $value =~ /^last_log/ ) {
-      return Frontier::Client->new->date_time( $svc_acct->$value() ); #conversion?
+      return Frontier::RPC2::DateTime::ISO8601->new( $svc_acct->$value() ); #conversion?
     } else {
-      return Frontier::Client->new->string( $svc_acct->$value() );
+      return Frontier::RPC2::String->new( $svc_acct->$value() );
     }
   } elsif ( $value eq 'domain' ) {
-    return Frontier::Client->new->string( $svc_acct->domain );
+    return Frontier::RPC2::String->new( $svc_acct->domain );
   } elsif ( $value eq 'crypt_password' ) {
-    return Frontier::Client->new->string( $svc_acct->crypt_password( $self->option('crypt') ) );
+    return Frontier::RPC2::String->new( $svc_acct->crypt_password( $self->option('crypt') ) );
   } elsif ( $value eq 'ldap_password' ) {
-    return Frontier::Client->new->string( $svc_acct->ldap_password($self->option('crypt') ) );
+    return Frontier::RPC2::String->new( $svc_acct->ldap_password($self->option('crypt') ) );
   } elsif ( $value eq 'radius_groups' ) {
     my @radius_groups = $svc_acct->radius_groups;
     #XXX
index 6e56c99..d46a996 100644 (file)
@@ -18,9 +18,8 @@ tie %options, 'Tie::IxHash',
   'username'  => { label=>'Database username' },
   'password'  => { label=>'Database password' },
   'manager'   => { label=>'Manager name' },
-  'groupid'   => { label=>'Group ID', default=>'1' },
-  'service_prefix' => { label=>'Service name prefix' },
-  'nasnames'  => { label=>'NAS IDs/addresses' },
+  'template_name'   => { label=>'Template service name' },
+  'service_prefix'  => { label=>'Service name prefix' },
   'debug'     => { label=>'Enable debugging', type=>'checkbox' },
 ;
 
@@ -235,6 +234,14 @@ not, create one.  Then return its srvid.
 sub export_part_svc {
   my ($self, $part_svc, $dbh) = @_;
 
+  # if $dbh exists, use the existing transaction
+  # otherwise create our own and commit when finished
+  my $commit = 0;
+  if (!$dbh) {
+    $dbh = $self->connect;
+    $commit = 1;
+  }
+
   my $name = $self->option('service_prefix').$part_svc->svc;
 
   my %params = (
@@ -242,19 +249,22 @@ sub export_part_svc {
     'enableservice'   => 1,
     'nextsrvid'       => -1,
     'dailynextsrvid'  => -1,
+    # force price-related fields to zero
+    'unitprice'       => 0,
+    'unitpriceadd'    => 0,
+    'unitpricetax'    => 0,
+    'unitpriceaddtax' => 0,
   );
   my @fixed_groups;
   # use speed settings from fixed usergroups configured on this part_svc
   if ( my $psc = $part_svc->part_svc_column('usergroup') ) {
-    if ( $psc->columnflag eq 'F' )  {
-      # each part_svc really should only have one fixed group with non-null 
-      # speed settings, but go by priority order for consistency
-      @fixed_groups = 
-        sort { $a->priority <=> $b->priority }
-        grep { $_ }
-        map { FS::radius_group->by_key($_) }
-        split(/\s*,\s*/, $psc->columnvalue);
-    }
+    # each part_svc really should only have one fixed group with non-null 
+    # speed settings, but go by priority order for consistency
+    @fixed_groups = 
+      sort { $a->priority <=> $b->priority }
+      grep { $_ }
+      map { FS::radius_group->by_key($_) }
+      split(/\s*,\s*/, $psc->columnvalue);
   } # otherwise there are no fixed groups, so leave speed empty
 
   foreach (qw(down up)) {
@@ -275,76 +285,71 @@ sub export_part_svc {
   $sth->execute($name) or die $dbh->errstr;
   if ( $sth->rows > 1 ) {
     die "Multiple services with name '$name' found in Radius Manager.\n";
-  } elsif ( $sth->rows == 1 ) {
-    my $row = $sth->fetchrow_arrayref;
-    my $srvid = $row->[0];
-    warn "rm_services: updating srvid#$srvid\n" if $DEBUG;
-    $sth = $dbh->prepare(
-      'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) .
-      ' WHERE srvid = ?'
-    );
-    $sth->execute(values(%params), $srvid) or die $dbh->errstr;
-    return $srvid;
-  } else { # $sth->rows == 0
-    # create a new one
-    # but first... get the next available srvid
+
+  } elsif ( $sth->rows == 0 ) {
+    # leave this blank to disable creating new service defs
+    my $template_name = $self->option('template_name');
+
+    die "Can't create a new service profile--no template service specified.\n"
+      unless $template_name;
+
+    warn "rm_services: fetching template '$template_name'\n" if $DEBUG;
+    $sth = $dbh->prepare('SELECT * FROM rm_services WHERE srvname = ? LIMIT 1');
+    $sth->execute($template_name);
+    die "Can't create a new service profile--template service ".
+      "'$template_name' not found.\n" unless $sth->rows == 1;
+    my $template = $sth->fetchrow_hashref;
+    %params = (%$template, %params);
+
+    # get the next available srvid
     $sth = $dbh->prepare('SELECT MAX(srvid) FROM rm_services');
     $sth->execute or die $dbh->errstr;
-    my $srvid = 1; # just in case you somehow have nothing in your database
+    my $srvid;
     if ( $sth->rows ) {
       $srvid = $sth->fetchrow_arrayref->[0] + 1;
     }
     $params{'srvid'} = $srvid;
-    # NOW create a new one
+
+    # create a new one based on the template
     warn "rm_services: inserting '$name' as srvid#$srvid\n" if $DEBUG;
     $sth = $dbh->prepare(
       'INSERT INTO rm_services ('.join(', ', keys %params).
       ') VALUES ('.join(', ', map {'?'} keys %params).')'
     );
     $sth->execute(values(%params)) or die $dbh->errstr;
-    # also link it to our manager name
+    # also link it to all the managers allowed on the template service
     warn "rm_services: linking to manager\n" if $DEBUG;
     $sth = $dbh->prepare(
-      'INSERT INTO rm_allowedmanagers (srvid, managername) VALUES (?, ?)'
+      'INSERT INTO rm_allowedmanagers (srvid, managername) '.
+      'SELECT ?, managername FROM rm_allowedmanagers WHERE srvid = ?'
     );
-    $sth->execute($srvid, $self->option('manager')) or die $dbh->errstr;
-    # and allow it on our NAS
+    $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr;
+    # and the same for NASes
+    warn "rm_services: linking to nas\n" if $DEBUG;
     $sth = $dbh->prepare(
-      'INSERT INTO rm_allowednases (srvid, nasid) VALUES (?, ?)'
+      'INSERT INTO rm_allowednases (srvid, nasid) '.
+      'SELECT ?, nasid FROM rm_allowednases WHERE srvid = ?'
     );
-    foreach my $nasid ($self->nas_ids($dbh)) {
-      warn "rm_services: linking to nasid#$nasid\n" if $DEBUG;
-      $sth->execute($srvid, $nasid) or die $dbh->errstr;
-    }
-    return $srvid;
-  }
-}
+    $sth->execute($srvid, $template->{srvid}) or die $dbh->errstr;
 
-=item nas_ids DBH
+    $dbh->commit if $commit;
+    return $srvid;
 
-Convert the 'nasnames  option into a list of real NAS ids.
+  } else { # $sth->rows == 1, it already exists
 
-=cut
-
-sub nas_ids {
-  my $self = shift;
-  my $dbh = shift;
+    my $row = $sth->fetchrow_arrayref;
+    my $srvid = $row->[0];
+    warn "rm_services: updating srvid#$srvid\n" if $DEBUG;
+    $sth = $dbh->prepare(
+      'UPDATE rm_services SET '.join(', ', map {"$_ = ?"} keys %params) .
+      ' WHERE srvid = ?'
+    );
+    $sth->execute(values(%params), $srvid) or die $dbh->errstr;
 
-  my @nasnames = split(/\s*,\s*/, $self->option('nasnames'));
-  return unless @nasnames;
-  # pass these through unchanged
-  my @ids = grep { /^\d+$/ } @nasnames;
-  @nasnames = grep { not /^\d+$/ } @nasnames;
-  if ( @nasnames ) {
-    my $in_nasnames = join(',', map {$dbh->quote($_)} @nasnames);
+    $dbh->commit if $commit;
+    return $srvid;
 
-    my $sth = $dbh->prepare("SELECT id FROM nas WHERE nasname IN ($in_nasnames)");
-    $sth->execute or die $dbh->errstr;
-    my $rows = $sth->fetchall_arrayref;
-    push @ids, $_->[0] foreach @$rows;
   }
-
-  return @ids;
 }
 
 1;
index 0229e3a..ed31929 100644 (file)
@@ -30,22 +30,17 @@ FS::Record.  The following fields are currently supported:
 
 =over 4
 
-=item tagnum
+=item tagnum - primary key
 
-primary key
+=item tagname - tag name
 
-=item tagname
+=item tagdesc - description (can be longer than name)
 
-tagname
+=item tagcolor - HTML-style color to display this tag
 
-=item tagdesc
-
-tagdesc
-
-=item tagcolor
-
-tagcolor
+=item by_default - 'Y' to enable this tag on new customers
 
+=item disabled
 
 =back
 
@@ -111,6 +106,7 @@ sub check {
     || $self->ut_text('tagname')
     || $self->ut_textn('tagdesc')
     || $self->ut_textn('tagcolor')
+    || $self->ut_enum('by_default', [ '', 'Y' ] )
     || $self->ut_enum('disabled', [ '', 'Y' ] )
   ;
   return $error if $error;
@@ -120,6 +116,21 @@ sub check {
 
 =back
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item default_tags
+
+Returns the tagnums of all tags that have 'by_default' enabled.
+
+=cut
+
+sub default_tags {
+  my $class = shift;
+  map { $_->tagnum } qsearch('part_tag', { disabled => '', by_default => 'Y' });
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
diff --git a/FS/bin/freeside-wkhtmltopdf b/FS/bin/freeside-wkhtmltopdf
new file mode 100755 (executable)
index 0000000..c6c5531
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if [ $DISPLAY ] ; then
+  wkhtmltopdf $@
+else
+  xvfb-run -- wkhtmltopdf $@
+fi
index 1ab52e5..1a1e60d 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/class_Common.html',
-              'name'   => 'Contact Type',
+              'name_singular'   => 'Contact Type',
               'table'  => 'contact_class',
              'nocat' => 1,
               'addl_labels' => { 'classnum'  => 'Type number',
index 8fce905..2b074aa 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/class_Common.html',
-              'name'        => 'Customer Class',
+              'name_singular'        => 'Customer Class',
               'table'       => 'cust_class',
               'addl_fields' => \@addl_fields,
               'addl_labels' => { 'tax' => 'Tax Exempt' },
index e3e812f..2628b4e 100755 (executable)
@@ -315,6 +315,8 @@ if ( $cgi->param('error') ) {
   $stateid = '';
   $payinfo = '';
 
+  $cgi->param('tagnum', FS::part_tag->default_tags);
+
   if ( $cgi->param('qualnum') =~ /^(\d+)$/ ) {
     my $qualnum = $1;
     my $qual = qsearchs('qual', { 'qualnum' => $qualnum } )
index 111190b..a7e4739 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/class_Common.html',
-              'name'   => 'Customer Note Class',
+              'name_singular'   => 'Customer Note Class',
               'table'  => 'cust_note_class',
              'nocat' => 1,
           )
index 8760dd8..26f487d 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/edit.html',
-                 'name'   => 'Hardware Class',
+                 'name_singular'   => 'Hardware Class',
                  'table'  => 'hardware_class',
                  'labels' => { 
                                'classnum'  => 'Class number',
index 3ab47fe..ddde557 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/edit.html',
-                 'name'   => 'Inventory Class',
+                 'name_singular'   => 'Inventory Class',
                  'table'  => 'inventory_class',
                  'labels' => { 
                                'classnum'  => 'Class number',
index 0d9a007..7832bd4 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/class_Common.html',
-              'name'   => 'Service class',
+              'name_singular'   => 'Service class',
               'table'  => 'part_svc_class',
              'nocat' => 1,
           )
index 2caeb27..5712560 100644 (file)
@@ -5,12 +5,14 @@
                 { field=>'tagname',  type=>'text', size=>10 },
                 { field=>'disabled', type=>'checkbox', value=>'Y' },
                 { field=>'tagdesc',  type=>'text', size=>60 },
+                { field=>'by_default',  type=>'checkbox', value=>'Y' },
                 $tagcolor,
               ],
               'labels'        => { 'tagnum'   => 'Tag #',
                                    'tagname'  => 'Tag',
                                    'tagdesc'  => 'Message',
                                    'tagcolor' => 'Highlight Color',
+                                   'by_default' => 'On by default',
                                    'disabled' => 'Disabled',
                                  },
               'viewall_dir' => 'browse',
index c4e3d8a..95c6f30 100644 (file)
@@ -1,5 +1,5 @@
 <% include( 'elements/class_Common.html',
-              'name'   => 'Package Class',
+              'name_singular'   => 'Package Class',
               'table'  => 'pkg_class',
               %opt,
           )
index b19f5c5..e146adf 100644 (file)
@@ -29,6 +29,8 @@ sub args_callback {
   # no validation of these; they can contain just about anything
   $content{'subject'} = $cgi->param('subject') || '';
   $content{'body'} = $cgi->param('body') || '';
+  $object->subject('');
+  $object->body('');
   return %content;
 }
 
index 5312644..76b1b71 100644 (file)
@@ -28,7 +28,7 @@ my $cgi = $opt{'cgi'};
 my $is_report = $opt{'is_report'};
 
 my @curr_tagnum = ();
-if ( $cgi && $cgi->param('error') ) {
+if ( $cgi && $cgi->param('tagnum') ) {
   @curr_tagnum = $cgi->param('tagnum');
 } elsif ( $opt{'custnum'} ) {
   @curr_tagnum = map $_->tagnum,