3efc26cc3f6f2aa50aa5e51dd37472b419aa1a9c
[freeside.git] / httemplate / edit / part_pkg.cgi
1 <% include( 'elements/edit.html',
2               'post_url'         => popurl(1).'process/part_pkg.cgi',
3               'name'             => "Package definition",
4               'table'            => 'part_pkg',
5
6               'agent_virt'       => 1,
7               'agent_null_right' => $edit_global,
8
9               #'viewall_dir'      => 'browse',
10               'viewall_url'      => $p.'browse/part_pkg.cgi',
11               'html_init'        => include('/elements/init_overlib.html').
12                                     $freq_changed,
13               'html_bottom'      => $html_bottom,
14
15               'begin_callback'       => $begin_callback,
16               'end_callback'         => $end_callback,
17               'new_hashref_callback' => $new_hashref_callback,
18               'new_object_callback'  => $new_object_callback,
19               'new_callback'         => $new_callback,
20               'clone_callback'       => $clone_callback,
21               'edit_callback'        => $edit_callback,
22               'error_callback'       => $error_callback,
23
24               'labels' => { 
25                             'pkgpart'          => 'Package Definition',
26                             'pkg'              => 'Package (customer-visible)',
27                             'comment'          => 'Comment (customer-hidden)',
28                             'classnum'         => 'Package class',
29                             'promo_code'       => 'Promotional code',
30                             'freq'             => 'Recurring fee frequency',
31                             'setuptax'         => 'Setup fee tax exempt',
32                             'recurtax'         => 'Recurring fee tax exempt',
33                             'taxclass'         => 'Tax class',
34                             'taxproduct_select'=> 'Tax products',
35                             'plan'             => 'Price plan',
36                             'disabled'         => 'Disable new orders',
37                             'pay_weight'       => 'Payment weight',
38                             'credit_weight'    => 'Credit weight',
39                             'agentnum'         => 'Agent',
40                             'setup_fee'        => 'Setup fee',
41                             'recur_fee'        => 'Recurring fee',
42                             'bill_dst_pkgpart' => 'Include line item(s) from package',
43                             'svc_dst_pkgpart'  => 'Include services of package',
44                           },
45
46               'fields' => [
47                             { field=>'clone',  type=>'hidden',
48                               curr_value_callback =>
49                                 sub { shift->param('clone') },
50                             },
51                             { field=>'pkgnum', type=>'hidden',
52                               curr_value_callback =>
53                                 sub { shift->param('pkgnum') },
54                             },
55
56                             { type => 'columnstart' },
57                             
58                               { field     => 'pkg',
59                                 type      => 'text',
60                                 size      => 40, #32
61                                 maxlength => 50,
62                               },
63                               {field=>'comment',  type=>'text', size=>40 }, #32
64                               { field         => 'agentnum',
65                                 type          => 'select-agent',
66                                 disable_empty => ! $acl_edit_global,
67                                 empty_label   => '(global)',
68                               },
69                               {field=>'classnum', type=>'select-pkg_class' },
70                               {field=>'disabled', type=>$disabled_type, value=>'Y'},
71
72                               { type  => 'tablebreak-tr-title',
73                                 value => 'Pricing', #better name?
74                               },
75                               { field => 'plan',
76                                 type  => 'selectlayers-select',
77                                 options => [ keys %plan_labels ],
78                                 labels  => \%plan_labels,
79                               },
80                               { field => 'setup_fee',
81                                 type  => 'money',
82                               },
83                               { field    => 'freq',
84                                 type     => 'part_pkg_freq',
85                                 onchange => 'freq_changed',
86                               },
87                               { field    => 'recur_fee',
88                                 type     => 'money',
89                                 disabled => sub { $recur_disabled },
90                               },
91                                 
92                               #price plan
93                               #setup fee
94                               #recurring frequency
95                               #recurring fee (auto-disable)
96
97                             { type => 'columnnext' },
98
99                               {type=>'justtitle', value=>'Taxation' },
100                               {field=>'setuptax', type=>'checkbox', value=>'Y'},
101                               {field=>'recurtax', type=>'checkbox', value=>'Y'},
102                               {field=>'taxclass', type=>'select-taxclass' },
103                               { field => 'taxproductnums',
104                                 type  => 'hidden',
105                                 value => join(',', @taxproductnums),
106                               },
107                               { field => 'taxproduct_select',
108                                 type  => 'selectlayers',
109                                 options => [ '(default)', @taxproductnums ],
110                                 curr_value => '(default)',
111                                 labels  => { ( '(default)' => '(default)' ),
112                                              map {($_=>$usage_class{$_})}
113                                              @taxproductnums
114                                            },
115                                 layer_fields => \%taxproduct_fields,
116                                 layer_values_callback => $taxproduct_values,
117                                 layers_only  =>   !$taxproducts,
118                                 cell_style   => ( !$taxproducts
119                                                   ? 'display:none'
120                                                   : ''
121                                                 ),
122                               },
123
124                               { type  => 'tablebreak-tr-title',
125                                 value => 'Promotions', #better name?
126                               },
127                               { field=>'promo_code', type=>'text', size=>15 },
128
129                               { type  => 'tablebreak-tr-title',
130                                 value => 'Line-item revenue recogition', #better name?
131                               },
132                               { field=>'pay_weight',    type=>'text', size=>6 },
133                               { field=>'credit_weight', type=>'text', size=>6 },
134
135                             { type => 'columnnext' },
136
137                               { field    => 'agent_type',
138                                 type     => 'select-agent_types',
139                                 disabled => ! $acl_edit_global,
140                                 curr_value_callback => sub {
141                                   my($cgi, $object, $field) = @_;
142                                   #in the other callbacks..?  hmm.
143                                   \@agent_type;
144                                 },
145                               },
146
147                             { type => 'columnend' },
148
149                             { 'type'  => 'tablebreak-tr-title',
150                               'value' => 'Pricing add-ons',
151                             },
152                             { 'field'      => 'bill_dst_pkgpart',
153                               'type'       => 'select-part_pkg',
154                               'm2_label'   => 'Include line item(s) from package',
155                               'm2m_method' => 'bill_part_pkg_link',
156                               'm2m_dstcol' => 'dst_pkgpart',
157                               'm2_error_callback' =>
158                                 &{$m2_error_callback_maker}('bill'),
159                             },
160
161                             { type  => 'tablebreak-tr-title',
162                               value => 'Services',
163                             },
164                             { type => 'pkg_svc', },
165
166                             { 'field'      => 'svc_dst_pkgpart',
167                               'label'      => 'Also include services from package: ',
168                               'type'       => 'select-part_pkg',
169                               'm2_label'   => 'Include services of package: ',
170                               'm2m_method' => 'svc_part_pkg_link',
171                               'm2m_dstcol' => 'dst_pkgpart',
172                               'm2_error_callback' =>
173                                 &{$m2_error_callback_maker}('svc'),
174                             },
175
176                             { type  => 'tablebreak-tr-title',
177                               value => 'Price plan options',
178                             },
179
180                           ],
181
182            )
183 %>
184 <%init>
185
186 my $curuser = $FS::CurrentUser::CurrentUser;
187
188 my $edit_global = 'Edit global package definitions';
189 my $acl_edit        = $curuser->access_right('Edit package definitions');
190 my $acl_edit_global = $curuser->access_right($edit_global);
191
192 my $acl_edit_either = $acl_edit || $acl_edit_global;
193
194 my $begin_callback = sub {
195   my( $cgi, $fields, $opt ) = @_;
196   die "access denied"
197     unless $acl_edit_either
198         || ( $cgi->param('pkgnum')
199              && $curuser->access_right('Customize customer package')
200            );
201 };
202
203 my $disabled_type = $acl_edit_either ? 'checkbox' : 'hidden';
204
205 my $conf = new FS::Conf;
206 my $taxproducts = $conf->exists('enable_taxproducts');
207
208 #XXX
209 # - tr-part_pkg_freq: month_increments_only (from price plans)
210 # - test cloning
211 # - test errors cloning
212 # - test custom pricing
213 # - move the selectlayer divs away from lame layer_callback
214
215 #my ($query) = $cgi->keywords;
216 #
217 #my $part_pkg = '';
218
219 my @agent_type = ();
220 my %tax_override = ();
221
222 my %taxproductnums = map { ($_->classnum => 1) }
223                      qsearch('usage_class', { 'disabled' => '' });
224
225 my %options = ();
226 my $recur_disabled = 1;
227
228 my $error_callback = sub {
229   my($cgi, $object, $fields, $opt ) = @_;
230
231   (@agent_type) = $cgi->param('agent_type');
232
233   $opt->{action} = 'Custom' if $cgi->param('clone');
234
235   $recur_disabled = $cgi->param('freq') ? 0 : 1;
236
237   foreach ($cgi->param) {
238     /^usage_taxproductnum_(\d+)$/ && ($taxproductnums{$1} = 1);
239   }
240   $tax_override{''} = $cgi->param('tax_override');
241   $tax_override{$_} = $cgi->param('tax_override_$_')
242     foreach(grep { /^tax_override_(\w+)$/ } $cgi->param);
243
244   #some false laziness w/process
245   $cgi->param('plan') =~ /^(\w+)$/ or die 'unparsable plan';
246   my $plan = $1;
247   my $options = $cgi->param($plan."__OPTIONS");
248   my @options = split(',', $options);
249   %options =
250     map { my $optionname = $_;
251           my $param = $plan."__$optionname";
252           my $value = join(', ', $cgi->param($param));
253           ( $optionname => $value );
254         }
255         @options;
256
257   #$cgi->param($_, $options{$_}) foreach (qw( setup_fee recur_fee ));
258   $object->set($_ => scalar($cgi->param($_)) )
259     foreach (qw( setup_fee recur_fee ));
260
261 };
262
263 my $new_hashref_callback = sub { { 'plan' => 'flat' }; };
264
265 my $new_object_callback = sub {
266   my( $cgi, $hashref, $fields, $opt ) = @_;
267
268   my $part_pkg = FS::part_pkg->new( $hashref );
269   $part_pkg->set($_ => '0')
270     foreach (qw( setup_fee recur_fee ));
271
272   $part_pkg;
273
274 };
275
276 my $edit_callback = sub {
277   my( $cgi, $object, $fields, $opt ) = @_;
278
279   $recur_disabled = $object->freq ? 0 : 1;
280
281   (@agent_type) = map {$_->typenum} qsearch('type_pkgs',{'pkgpart'=>$1});
282
283   foreach ($object->options) {
284     /^usage_taxproductnum_(\d+)$/ && ($taxproductnums{$1} = 1);
285   }
286   foreach ($object->part_pkg_taxoverride) {
287     $taxproductnums{$_->usage_class} = 1
288       if $_->usage_class;
289   }
290
291   %options = $object->options;
292
293   $object->set($_ => $object->option($_))
294     foreach (qw( setup_fee recur_fee ));
295
296 };
297
298 my $new_callback = sub {
299   my( $cgi, $object, $fields ) = @_;
300
301   my $conf = new FS::Conf; 
302   if ( $conf->exists('agent_defaultpkg') ) {
303     #my @all_agent_types = map {$_->typenum} qsearch('agent_type',{});
304     @agent_type = map {$_->typenum} qsearch('agent_type',{});
305   }
306
307 };
308
309 my $clone_callback = sub {
310   my( $cgi, $object, $fields, $opt ) = @_;
311
312   $opt->{action} = 'Custom';
313
314   #my $part_pkg = $clone_part_pkg->clone;
315   #this is all clone did anyway
316   $object->comment( '(CUSTOM) '. $object->comment )
317     unless $object->comment =~ /^\(CUSTOM\) /;
318
319   $object->disabled('Y');
320
321   %options = $object->options;
322
323   $object->set($_ => $options{$_})
324     foreach (qw( setup_fee recur_fee ));
325
326   $recur_disabled = $object->freq ? 0 : 1;
327 };
328
329 my $m2_error_callback_maker = sub {
330   my $link_type = shift; #yay closures
331   return sub {
332     my( $cgi, $object ) = @_;
333       map  {
334              new FS::part_pkg_link {
335                'link_type'   => $link_type,
336                'src_pkgpart' => $object->pkgpart,
337                'dst_pkgpart' => $_,
338              };
339            }
340       grep $_,
341       map  $cgi->param($_),
342       grep /^${link_type}_dst_pkgpart(\d+)$/, $cgi->param;
343   };
344 };
345
346 my $freq_changed = <<'END';
347   <SCRIPT TYPE="text/javascript">
348
349     function freq_changed(what) {
350       var freq = what.options[what.selectedIndex].value;
351
352       if ( freq == '0' ) {
353         what.form.recur_fee.disabled = true;
354         what.form.recur_fee.style.backgroundColor = '#dddddd';
355       } else {
356         what.form.recur_fee.disabled = false;
357         what.form.recur_fee.style.backgroundColor = '#ffffff';
358       }
359
360     }
361
362   </SCRIPT>
363 END
364
365 tie my %plans, 'Tie::IxHash', %{ FS::part_pkg::plan_info() };
366
367 tie my %plan_labels, 'Tie::IxHash',
368   map {  $_ => ( $plans{$_}->{'shortname'} || $plans{$_}->{'name'} ) }
369       keys %plans;
370
371 my $html_bottom = sub {
372   my( $object ) = @_;
373
374   #warn join("\n", map { "$_: $options{$_}" } keys %options ). "\n";
375
376   my $layer_callback = sub {
377   
378     my $layer = shift;
379     my $html = ntable("#cccccc",2);
380   
381     #$html .= '
382     #  <TR>
383     #    <TD ALIGN="right">Recurring fee frequency </TD>
384     #    <TD><SELECT NAME="freq">
385     #';
386     #
387     #my @freq = keys %freq;
388     #@freq = grep { /^\d+$/ } @freq
389   #XXX this bit#  #  if exists($plans{$layer}->{'freq'}) && $plans{$layer}->{'freq'} eq 'm';
390     #foreach my $freq ( @freq ) {
391     #  $html .= qq(<OPTION VALUE="$freq");
392     #  $html .= ' SELECTED' if $freq eq $part_pkg->freq;
393     #  $html .= ">$freq{$freq}";
394     #}
395     #$html .= '</SELECT></TD></TR>';
396   
397     my $href = $plans{$layer}->{'fields'};
398     my @fields = exists($plans{$layer}->{'fieldorder'})
399                    ? @{$plans{$layer}->{'fieldorder'}}
400                    : keys %{ $href };
401   
402     foreach my $field ( grep $_ !~ /^(setup|recur)_fee$/, @fields ) {
403   
404       $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
405   
406       my $format = sub { shift };
407       $format = $href->{$field}{'format'} if exists($href->{$field}{'format'});
408
409       #XXX these should use elements/ fields... (or this whole thing should
410       #just use layer_fields instead of layer_callback)
411   
412       if ( ! exists($href->{$field}{'type'}) ) {
413   
414         $html .= qq!<INPUT TYPE="text" NAME="${layer}__$field" VALUE="!.
415                  ( exists($options{$field})
416                      ? &$format($options{$field})
417                      : $href->{$field}{'default'} ).
418                  qq!">!;
419   
420       } elsif ( $href->{$field}{'type'} eq 'checkbox' ) {
421   
422         $html .= qq!<INPUT TYPE="checkbox" NAME="${layer}__$field" VALUE=1 !.
423                  ( exists($options{$field}) && $options{$field}
424                    ? ' CHECKED'
425                    : ''
426                  ). '>';
427   
428       } elsif ( $href->{$field}{'type'} =~ /^select/ ) {
429   
430         $html .= '<SELECT';
431         $html .= ' MULTIPLE'
432           if $href->{$field}{'type'} eq 'select_multiple';
433         $html .= qq! NAME="${layer}__$field">!;
434   
435         if ( $href->{$field}{'select_table'} ) {
436           foreach my $record (
437             qsearch( $href->{$field}{'select_table'},
438                      $href->{$field}{'select_hash'}   )
439           ) {
440             my $value = $record->getfield($href->{$field}{'select_key'});
441             $html .= qq!<OPTION VALUE="$value"!.
442                      (  $options{$field} =~ /(^|, *)$value *(,|$)/ #?
443                           ? ' SELECTED'
444                           : ''
445                      ).
446                      '>'. $record->getfield($href->{$field}{'select_label'});
447           }
448         } elsif ( $href->{$field}{'select_options'} ) {
449           foreach my $key ( keys %{ $href->{$field}{'select_options'} } ) {
450             my $label = $href->{$field}{'select_options'}{$key};
451             $html .= qq!<OPTION VALUE="$key"!.
452                      ( $options{$field} =~ /(^|, *)$key *(,|$)/ #?
453                          ? ' SELECTED'
454                          : ''
455                      ).
456                      '>'. $label;
457           }
458   
459         } else {
460           $html .= '<font color="#ff0000">warning: '.
461                    "don't know how to retreive options for $field select field".
462                    '</font>';
463         }
464         $html .= '</SELECT>';
465   
466       } elsif ( $href->{$field}{'type'} eq 'radio' ) {
467   
468         my $radio =
469           qq!<INPUT TYPE="radio" NAME="${layer}__$field"!;
470   
471         foreach my $key ( keys %{ $href->{$field}{'options'} } ) {
472           my $label = $href->{$field}{'options'}{$key};
473           $html .= qq!$radio VALUE="$key"!.
474                    ( $options{$field} =~ /(^|, *)$key *(,|$)/ #?
475                        ? ' CHECKED'
476                        : ''
477                    ).
478                    "> $label<BR>";
479         }
480   
481       }
482   
483       $html .= '</TD></TR>';
484     }
485     $html .= '</TABLE>';
486   
487     $html .= qq(<INPUT TYPE="hidden" NAME="${layer}__OPTIONS" VALUE=").
488              join(',', keys %{ $href } ). '">';
489   
490     $html;
491   
492   };
493
494   my %selectlayers = (
495     field          => 'plan',
496     options        => [ keys %plan_labels ],
497     labels         => \%plan_labels,
498     curr_value     => $object->plan,
499     layer_callback => $layer_callback,
500   );
501
502   my $return =
503     include('/elements/selectlayers.html', %selectlayers, 'layers_only'=>1 ).
504     '<SCRIPT TYPE="text/javascript">'.
505       include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 );
506
507   $return .=
508     "taxproduct_selectchanged(document.getElementById('taxproduct_select'));\n"
509       if $taxproducts;
510
511   $return .= '</SCRIPT>';
512
513   $return;
514
515 };
516
517 my %usage_class = map { ($_->classnum => $_->classname) }
518                   qsearch('usage_class', {});
519 $usage_class{setup} = 'Setup';
520 $usage_class{recur} = 'Recurring';
521
522 my @taxproductnums = ();
523 my %taxproduct_fields = ();
524 my $end_callback = sub {
525   my( $cgi, $object, $fields, $opt ) = @_;
526
527   @taxproductnums = ( qw( setup recur ), sort (keys %taxproductnums) );
528
529   if ( $object->pkgpart ) {
530     foreach my $usage_class ( '', @taxproductnums ) {
531       $tax_override{$usage_class} =
532         join (",", map $_->taxclassnum,
533                        qsearch( 'part_pkg_taxoverride', {
534                                   'pkgpart'     => $object->pkgpart,
535                                   'usage_class' => $usage_class,
536                               })
537              );
538     }
539   }
540
541   %taxproduct_fields =
542     map { $_ => [ "taxproductnum_$_", 
543                   { type  => 'select-taxproduct',
544                     #label => "$usage_class{$_} tax product",
545                   },
546                   "tax_override_$_", 
547                   { type  => 'select-taxoverride' }
548                 ]
549         }
550         @taxproductnums;
551
552   $taxproduct_fields{'(default)'} =
553     [ 'taxproductnum', { type => 'select-taxproduct',
554                          #label => 'Default tax product',
555                        },
556       'tax_override',  { type => 'select-taxoverride' },
557     ];
558 };
559
560 my $taxproduct_values = sub {
561   my ($cgi, $object, $flags) = @_;
562   my $routine =
563     sub { my $layer = shift;
564           my @fields = @{$taxproduct_fields{$layer}};
565           my @values = ();
566           while( @fields ) {
567             my $field = shift @fields;
568             shift @fields;
569             $field =~ /^taxproductnum_\w+$/ &&
570               push @values, ( $field => $options{"usage_$field"} );
571             $field =~ /^tax_override_(\w+)$/ &&
572               push @values, ( $field => $tax_override{$1} );
573             $field =~ /^taxproductnum$/ &&
574               push @values, ( $field => $object->taxproductnum );
575             $field =~ /^tax_override$/ &&
576               push @values, ( $field => $tax_override{''} );
577           }
578           { (@values) };
579         };
580   
581   my @result = 
582     map { ( $_ => { &{$routine}($_) } ) } ( '(default)', @taxproductnums );
583   return({ @result });
584   
585 };
586
587 </%init>