Auftrags-Controller: Workflow -> Rechnung (Speichern und Rechnung erfassen)
[kivitendo-erp.git] / SL / Controller / Order.pm
1 package SL::Controller::Order;
2
3 use strict;
4 use parent qw(SL::Controller::Base);
5
6 use SL::Helper::Flash qw(flash_later);
7 use SL::Presenter::Tag qw(select_tag);
8 use SL::Locale::String qw(t8);
9 use SL::SessionFile::Random;
10 use SL::PriceSource;
11 use SL::Webdav;
12 use SL::File;
13
14 use SL::DB::Order;
15 use SL::DB::Default;
16 use SL::DB::Unit;
17 use SL::DB::Part;
18 use SL::DB::Printer;
19 use SL::DB::Language;
20
21 use SL::Helper::CreatePDF qw(:all);
22 use SL::Helper::PrintOptions;
23
24 use SL::Controller::Helper::GetModels;
25
26 use List::Util qw(first);
27 use List::MoreUtils qw(none pairwise first_index);
28 use English qw(-no_match_vars);
29 use File::Spec;
30
31 use Rose::Object::MakeMethods::Generic
32 (
33  scalar => [ qw(item_ids_to_delete) ],
34  'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
35 );
36
37
38 # safety
39 __PACKAGE__->run_before('_check_auth');
40
41 __PACKAGE__->run_before('_recalc',
42                         only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
43
44 __PACKAGE__->run_before('_get_unalterable_data',
45                         only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
46
47 #
48 # actions
49 #
50
51 # add a new order
52 sub action_add {
53   my ($self) = @_;
54
55   $self->order->transdate(DateTime->now_local());
56   $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
57
58   $self->_pre_render();
59   $self->render(
60     'order/form',
61     title => $self->type eq _sales_order_type()    ? $::locale->text('Add Sales Order')
62            : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
63            : '',
64     %{$self->{template_args}}
65   );
66 }
67
68 # edit an existing order
69 sub action_edit {
70   my ($self) = @_;
71
72   $self->_load_order;
73   $self->_recalc();
74   $self->_pre_render();
75   $self->render(
76     'order/form',
77     title => $self->type eq _sales_order_type()    ? $::locale->text('Edit Sales Order')
78            : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
79            : '',
80     %{$self->{template_args}}
81   );
82 }
83
84 # delete the order
85 sub action_delete {
86   my ($self) = @_;
87
88   my $errors = $self->_delete();
89
90   if (scalar @{ $errors }) {
91     $self->js->flash('error', $_) foreach @{ $errors };
92     return $self->js->render();
93   }
94
95   flash_later('info', $::locale->text('The order has been deleted'));
96   my @redirect_params = (
97     action => 'add',
98     type   => $self->type,
99   );
100
101   $self->redirect_to(@redirect_params);
102 }
103
104 # save the order
105 sub action_save {
106   my ($self) = @_;
107
108   my $errors = $self->_save();
109
110   if (scalar @{ $errors }) {
111     $self->js->flash('error', $_) foreach @{ $errors };
112     return $self->js->render();
113   }
114
115   flash_later('info', $::locale->text('The order has been saved'));
116   my @redirect_params = (
117     action => 'edit',
118     type   => $self->type,
119     id     => $self->order->id,
120   );
121
122   $self->redirect_to(@redirect_params);
123 }
124
125 # print the order
126 #
127 # This is called if "print" is pressed in the print dialog.
128 # If PDF creation was requested and succeeded, the pdf is stored in a session
129 # file and the filename is stored as session value with an unique key. A
130 # javascript function with this key is then called. This function calls the
131 # download action below (action_download_pdf), which offers the file for
132 # download.
133 sub action_print {
134   my ($self) = @_;
135
136   my $format      = $::form->{print_options}->{format};
137   my $media       = $::form->{print_options}->{media};
138   my $formname    = $::form->{print_options}->{formname};
139   my $copies      = $::form->{print_options}->{copies};
140   my $groupitems  = $::form->{print_options}->{groupitems};
141
142   # only pdf by now
143   if (none { $format eq $_ } qw(pdf)) {
144     return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
145   }
146
147   # only screen or printer by now
148   if (none { $media eq $_ } qw(screen printer)) {
149     return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
150   }
151
152   my $language;
153   $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
154
155   my $form = Form->new;
156   $form->{ordnumber} = $self->order->ordnumber;
157   $form->{type}      = $self->type;
158   $form->{format}    = $format;
159   $form->{formname}  = $formname;
160   $form->{language}  = '_' . $language->template_code if $language;
161   my $pdf_filename   = $form->generate_attachment_filename();
162
163   my $pdf;
164   my @errors = _create_pdf($self->order, \$pdf, { format     => $format,
165                                                   formname   => $formname,
166                                                   language   => $language,
167                                                   groupitems => $groupitems });
168   if (scalar @errors) {
169     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
170   }
171
172   if ($media eq 'screen') {
173     # screen/download
174     my $sfile = SL::SessionFile::Random->new(mode => "w");
175     $sfile->fh->print($pdf);
176     $sfile->fh->close;
177
178     my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
179     $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
180
181     $self->js
182     ->run('kivi.Order.download_pdf', $pdf_filename, $key)
183     ->flash('info', t8('The PDF has been created'));
184
185   } elsif ($media eq 'printer') {
186     # printer
187     my $printer_id = $::form->{print_options}->{printer_id};
188     SL::DB::Printer->new(id => $printer_id)->load->print_document(
189       copies  => $copies,
190       content => $pdf,
191     );
192
193     $self->js->flash('info', t8('The PDF has been printed'));
194   }
195
196   # copy file to webdav folder
197   if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
198     my $webdav = SL::Webdav->new(
199       type     => $self->type,
200       number   => $self->order->ordnumber,
201     );
202     my $webdav_file = SL::Webdav::File->new(
203       webdav   => $webdav,
204       filename => $pdf_filename,
205     );
206     eval {
207       $webdav_file->store(data => \$pdf);
208       1;
209     } or do {
210       $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
211     }
212   }
213   if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
214     eval {
215       SL::File->save(object_id     => $self->order->id,
216                      object_type   => $self->type,
217                      mime_type     => 'application/pdf',
218                      source        => 'created',
219                      file_type     => 'document',
220                      file_name     => $pdf_filename,
221                      file_contents => $pdf);
222       1;
223     } or do {
224       $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
225     }
226   }
227   $self->js->render;
228 }
229
230 # offer pdf for download
231 #
232 # It needs to get the key for the session value to get the pdf file.
233 sub action_download_pdf {
234   my ($self) = @_;
235
236   my $key = $::form->{key};
237   my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
238   return $self->send_file(
239     $tmp_filename,
240     type => 'application/pdf',
241     name => $::form->{pdf_filename},
242   );
243 }
244
245 # open the email dialog
246 sub action_show_email_dialog {
247   my ($self) = @_;
248
249   my $cv_method = $self->cv;
250
251   if (!$self->order->$cv_method) {
252     return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
253                     ->render($self);
254   }
255
256   $self->{email}->{to}   = $self->order->contact->cp_email if $self->order->contact;
257   $self->{email}->{to} ||= $self->order->$cv_method->email;
258   $self->{email}->{cc}   = $self->order->$cv_method->cc;
259   $self->{email}->{bcc}  = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
260   # Todo: get addresses from shipto, if any
261
262   my $form = Form->new;
263   $form->{ordnumber} = $self->order->ordnumber;
264   $form->{formname}  = $self->type;
265   $form->{type}      = $self->type;
266   $form->{language} = 'de';
267   $form->{format}   = 'pdf';
268
269   $self->{email}->{subject}             = $form->generate_email_subject();
270   $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
271   $self->{email}->{message}             = $form->create_email_signature();
272
273   my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
274   $self->js
275       ->run('kivi.Order.show_email_dialog', $dialog_html)
276       ->reinit_widgets
277       ->render($self);
278 }
279
280 # send email
281 #
282 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
283 sub action_send_email {
284   my ($self) = @_;
285
286   my $mail      = Mailer->new;
287   $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
288   $mail->{$_}   = $::form->{email}->{$_} for qw(to cc bcc subject message);
289
290   my $pdf;
291   my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
292   if (scalar @errors) {
293     return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
294   }
295
296   $mail->{attachments} = [{ "content" => $pdf,
297                             "name"    => $::form->{email}->{attachment_filename} }];
298
299   if (my $err = $mail->send) {
300     return $self->js->flash('error', t8('Sending E-mail: ') . $err)
301                     ->render($self);
302   }
303
304   # internal notes
305   my $intnotes = $self->order->intnotes;
306   $intnotes   .= "\n\n" if $self->order->intnotes;
307   $intnotes   .= t8('[email]')                                                                                        . "\n";
308   $intnotes   .= t8('Date')       . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
309   $intnotes   .= t8('To (email)') . ": " . $mail->{to}                                                                . "\n";
310   $intnotes   .= t8('Cc')         . ": " . $mail->{cc}                                                                . "\n"    if $mail->{cc};
311   $intnotes   .= t8('Bcc')        . ": " . $mail->{bcc}                                                               . "\n"    if $mail->{bcc};
312   $intnotes   .= t8('Subject')    . ": " . $mail->{subject}                                                           . "\n\n";
313   $intnotes   .= t8('Message')    . ": " . $mail->{message};
314
315   $self->js
316       ->val('#order_intnotes', $intnotes)
317       ->run('kivi.Order.close_email_dialog')
318       ->render($self);
319 }
320
321 # save the order and redirect to the frontend subroutine for a new
322 # delivery order
323 sub action_save_and_delivery_order {
324   my ($self) = @_;
325
326   my $errors = $self->_save();
327
328   if (scalar @{ $errors }) {
329     $self->js->flash('error', $_) foreach @{ $errors };
330     return $self->js->render();
331   }
332   flash_later('info', $::locale->text('The order has been saved'));
333
334   my @redirect_params = (
335     controller => 'oe.pl',
336     action     => 'oe_delivery_order_from_order',
337     id         => $self->order->id,
338   );
339
340   $self->redirect_to(@redirect_params);
341 }
342
343 # save the order and redirect to the frontend subroutine for a new
344 # invoice
345 sub action_save_and_invoice {
346   my ($self) = @_;
347
348   my $errors = $self->_save();
349
350   if (scalar @{ $errors }) {
351     $self->js->flash('error', $_) foreach @{ $errors };
352     return $self->js->render();
353   }
354   flash_later('info', $::locale->text('The order has been saved'));
355
356   my @redirect_params = (
357     controller => 'oe.pl',
358     action     => 'oe_invoice_from_order',
359     id         => $self->order->id,
360   );
361
362   $self->redirect_to(@redirect_params);
363 }
364
365 # set form elements in respect to a changed customer or vendor
366 #
367 # This action is called on an change of the customer/vendor picker.
368 sub action_customer_vendor_changed {
369   my ($self) = @_;
370
371   my $cv_method = $self->cv;
372
373   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
374     $self->js->show('#cp_row');
375   } else {
376     $self->js->hide('#cp_row');
377   }
378
379   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
380     $self->js->show('#shipto_row');
381   } else {
382     $self->js->hide('#shipto_row');
383   }
384
385   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
386
387   if ($self->order->is_sales) {
388     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
389                               ? $self->order->$cv_method->taxincluded_checked
390                               : $::myconfig{taxincluded_checked});
391     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
392   }
393
394   $self->order->payment_id($self->order->$cv_method->payment_id);
395   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
396
397   $self->_recalc();
398
399   $self->js
400     ->replaceWith('#order_cp_id',            $self->build_contact_select)
401     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
402     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
403     ->val(        '#order_taxincluded',      $self->order->taxincluded)
404     ->val(        '#order_payment_id',       $self->order->payment_id)
405     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
406     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
407     ->focus(      '#order_' . $self->cv . '_id');
408
409   $self->_js_redisplay_amounts_and_taxes;
410   $self->js->render();
411 }
412
413 # called if a unit in an existing item row is changed
414 sub action_unit_changed {
415   my ($self) = @_;
416
417   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
418   my $item = $self->order->items_sorted->[$idx];
419
420   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
421   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
422
423   $self->_recalc();
424
425   $self->js
426     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
427   $self->_js_redisplay_line_values;
428   $self->_js_redisplay_amounts_and_taxes;
429   $self->js->render();
430 }
431
432 # add an item row for a new item entered in the input row
433 sub action_add_item {
434   my ($self) = @_;
435
436   my $form_attr = $::form->{add_item};
437
438   return unless $form_attr->{parts_id};
439
440   my $item = _new_item($self->order, $form_attr);
441
442   $self->order->add_items($item);
443
444   $self->_recalc();
445
446   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
447   my $row_as_html = $self->p->render('order/tabs/_row',
448                                      ITEM              => $item,
449                                      ID                => $item_id,
450                                      ALL_PRICE_FACTORS => $self->all_price_factors
451   );
452
453   $self->js
454     ->append('#row_table_id', $row_as_html);
455
456   if ( $item->part->is_assortment ) {
457     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
458     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
459       my $attr = { parts_id => $assortment_item->parts_id,
460                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
461                    unit     => $assortment_item->unit,
462                    description => $assortment_item->part->description,
463                  };
464       my $item = _new_item($self->order, $attr);
465
466       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
467       $item->discount(1) unless $assortment_item->charge;
468
469       $self->order->add_items( $item );
470       $self->_recalc();
471       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
472       my $row_as_html = $self->p->render('order/tabs/_row',
473                                          ITEM              => $item,
474                                          ID                => $item_id,
475                                          ALL_PRICE_FACTORS => $self->all_price_factors
476       );
477       $self->js
478         ->append('#row_table_id', $row_as_html);
479     };
480   };
481
482   $self->js
483     ->val('.add_item_input', '')
484     ->run('kivi.Order.init_row_handlers')
485     ->run('kivi.Order.row_table_scroll_down')
486     ->run('kivi.Order.renumber_positions')
487     ->focus('#add_item_parts_id_name');
488
489   $self->_js_redisplay_amounts_and_taxes;
490   $self->js->render();
491 }
492
493 # open the dialog for entering multiple items at once
494 sub action_show_multi_items_dialog {
495   require SL::DB::PartsGroup;
496   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
497                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
498 }
499
500 # update the filter results in the multi item dialog
501 sub action_multi_items_update_result {
502   my $max_count = 100;
503
504   $::form->{multi_items}->{filter}->{obsolete} = 0;
505
506   my $count = $_[0]->multi_items_models->count;
507
508   if ($count == 0) {
509     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
510     $_[0]->render($text, { layout => 0 });
511   } elsif ($count > $max_count) {
512     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
513     $_[0]->render($text, { layout => 0 });
514   } else {
515     my $multi_items = $_[0]->multi_items_models->get;
516     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
517                   multi_items => $multi_items);
518   }
519 }
520
521 # add item rows for multiple items at once
522 sub action_add_multi_items {
523   my ($self) = @_;
524
525   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
526   return $self->js->render() unless scalar @form_attr;
527
528   my @items;
529   foreach my $attr (@form_attr) {
530     my $item = _new_item($self->order, $attr);
531     push @items, $item;
532     if ( $item->part->is_assortment ) {
533       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
534         my $attr = { parts_id => $assortment_item->parts_id,
535                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
536                      unit     => $assortment_item->unit,
537                      description => $assortment_item->part->description,
538                    };
539         my $item = _new_item($self->order, $attr);
540
541         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
542         $item->discount(1) unless $assortment_item->charge;
543         push @items, $item;
544       }
545     }
546   }
547   $self->order->add_items(@items);
548
549   $self->_recalc();
550
551   foreach my $item (@items) {
552     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
553     my $row_as_html = $self->p->render('order/tabs/_row',
554                                        ITEM              => $item,
555                                        ID                => $item_id,
556                                        ALL_PRICE_FACTORS => $self->all_price_factors
557     );
558
559     $self->js->append('#row_table_id', $row_as_html);
560   }
561
562   $self->js
563     ->run('kivi.Order.close_multi_items_dialog')
564     ->run('kivi.Order.init_row_handlers')
565     ->run('kivi.Order.row_table_scroll_down')
566     ->run('kivi.Order.renumber_positions')
567     ->focus('#add_item_parts_id_name');
568
569   $self->_js_redisplay_amounts_and_taxes;
570   $self->js->render();
571 }
572
573 # recalculate all linetotals, amounts and taxes and redisplay them
574 sub action_recalc_amounts_and_taxes {
575   my ($self) = @_;
576
577   $self->_recalc();
578
579   $self->_js_redisplay_line_values;
580   $self->_js_redisplay_amounts_and_taxes;
581   $self->js->render();
582 }
583
584 # redisplay item rows if they are sorted by an attribute
585 sub action_reorder_items {
586   my ($self) = @_;
587
588   my %sort_keys = (
589     partnumber  => sub { $_[0]->part->partnumber },
590     description => sub { $_[0]->description },
591     qty         => sub { $_[0]->qty },
592     sellprice   => sub { $_[0]->sellprice },
593     discount    => sub { $_[0]->discount },
594   );
595
596   my $method = $sort_keys{$::form->{order_by}};
597   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
598   if ($::form->{sort_dir}) {
599     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
600   } else {
601     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
602   }
603   $self->js
604     ->run('kivi.Order.redisplay_items', \@to_sort)
605     ->render;
606 }
607
608 # show the popup to choose a price/discount source
609 sub action_price_popup {
610   my ($self) = @_;
611
612   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
613   my $item = $self->order->items_sorted->[$idx];
614
615   $self->render_price_dialog($item);
616 }
617
618 # get the longdescription for an item if the dialog to enter/change the
619 # longdescription was opened and the longdescription is empty
620 #
621 # If this item is new, get the longdescription from Part.
622 # Otherwise get it from OrderItem.
623 sub action_get_item_longdescription {
624   my $longdescription;
625
626   if ($::form->{item_id}) {
627     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
628   } elsif ($::form->{parts_id}) {
629     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
630   }
631   $_[0]->render(\ $longdescription, { type => 'text' });
632 }
633
634 # load the second row for one or more items
635 #
636 # This action gets the html code for all items second rows by rendering a template for
637 # the second row and sets the html code via client js.
638 sub action_load_second_rows {
639   my ($self) = @_;
640
641   $self->_recalc() if $self->order->is_sales; # for margin calculation
642
643   foreach my $item_id (@{ $::form->{item_ids} }) {
644     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
645     my $item = $self->order->items_sorted->[$idx];
646
647     $self->_js_load_second_row($item, $item_id, 0);
648   }
649
650   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
651
652   $self->js->render();
653 }
654
655 sub _js_load_second_row {
656   my ($self, $item, $item_id, $do_parse) = @_;
657
658   if ($do_parse) {
659     # Parse values from form (they are formated while rendering (template)).
660     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
661     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
662     foreach my $var (@{ $item->cvars_by_config }) {
663       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
664     }
665     $item->parse_custom_variable_values;
666   }
667
668   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item);
669
670   $self->js
671     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
672     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
673 }
674
675 sub _js_redisplay_line_values {
676   my ($self) = @_;
677
678   my $is_sales = $self->order->is_sales;
679
680   # sales orders with margins
681   my @data;
682   if ($is_sales) {
683     @data = map {
684       [
685        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
686        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
687        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
688       ]} @{ $self->order->items_sorted };
689   } else {
690     @data = map {
691       [
692        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
693       ]} @{ $self->order->items_sorted };
694   }
695
696   $self->js
697     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
698 }
699
700 sub _js_redisplay_amounts_and_taxes {
701   my ($self) = @_;
702
703   if (scalar @{ $self->{taxes} }) {
704     $self->js->show('#taxincluded_row_id');
705   } else {
706     $self->js->hide('#taxincluded_row_id');
707   }
708
709   if ($self->order->taxincluded) {
710     $self->js->hide('#subtotal_row_id');
711   } else {
712     $self->js->show('#subtotal_row_id');
713   }
714
715   $self->js
716     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
717     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
718     ->remove('.tax_row')
719     ->insertBefore($self->build_tax_rows, '#amount_row_id');
720 }
721
722 #
723 # helpers
724 #
725
726 sub init_valid_types {
727   [ _sales_order_type(), _purchase_order_type() ];
728 }
729
730 sub init_type {
731   my ($self) = @_;
732
733   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
734     die "Not a valid type for order";
735   }
736
737   $self->type($::form->{type});
738 }
739
740 sub init_cv {
741   my ($self) = @_;
742
743   my $cv = $self->type eq _sales_order_type()    ? 'customer'
744          : $self->type eq _purchase_order_type() ? 'vendor'
745          : die "Not a valid type for order";
746
747   return $cv;
748 }
749
750 sub init_p {
751   SL::Presenter->get;
752 }
753
754 sub init_order {
755   $_[0]->_make_order;
756 }
757
758 # model used to filter/display the parts in the multi-items dialog
759 sub init_multi_items_models {
760   SL::Controller::Helper::GetModels->new(
761     controller     => $_[0],
762     model          => 'Part',
763     with_objects   => [ qw(unit_obj) ],
764     disable_plugin => 'paginated',
765     source         => $::form->{multi_items},
766     sorted         => {
767       _default    => {
768         by  => 'partnumber',
769         dir => 1,
770       },
771       partnumber  => t8('Partnumber'),
772       description => t8('Description')}
773   );
774 }
775
776 sub init_all_price_factors {
777   SL::DB::Manager::PriceFactor->get_all;
778 }
779
780 sub _check_auth {
781   my ($self) = @_;
782
783   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
784
785   my $right   = $right_for->{ $self->type };
786   $right    ||= 'DOES_NOT_EXIST';
787
788   $::auth->assert($right);
789 }
790
791 # build the selection box for contacts
792 #
793 # Needed, if customer/vendor changed.
794 sub build_contact_select {
795   my ($self) = @_;
796
797   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
798     value_key  => 'cp_id',
799     title_key  => 'full_name_dep',
800     default    => $self->order->cp_id,
801     with_empty => 1,
802     style      => 'width: 300px',
803   );
804 }
805
806 # build the selection box for shiptos
807 #
808 # Needed, if customer/vendor changed.
809 sub build_shipto_select {
810   my ($self) = @_;
811
812   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
813     value_key  => 'shipto_id',
814     title_key  => 'displayable_id',
815     default    => $self->order->shipto_id,
816     with_empty => 1,
817     style      => 'width: 300px',
818   );
819 }
820
821 # build the rows for displaying taxes
822 #
823 # Called if amounts where recalculated and redisplayed.
824 sub build_tax_rows {
825   my ($self) = @_;
826
827   my $rows_as_html;
828   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
829     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
830   }
831   return $rows_as_html;
832 }
833
834
835 sub render_price_dialog {
836   my ($self, $record_item) = @_;
837
838   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
839
840   $self->js
841     ->run(
842       'kivi.io.price_chooser_dialog',
843       t8('Available Prices'),
844       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
845     )
846     ->reinit_widgets;
847
848 #   if (@errors) {
849 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
850 #     $self->js->show('#dialog_flash_error');
851 #   }
852
853   $self->js->render;
854 }
855
856 sub _load_order {
857   my ($self) = @_;
858
859   return if !$::form->{id};
860
861   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
862 }
863
864 # load or create a new order object
865 #
866 # And assign changes from the for to this object.
867 # If the order is loaded from db, check if items are deleted in the form,
868 # remove them form the object and collect them for removing from db on saving.
869 # Then create/update items from form (via _make_item) and add them.
870 sub _make_order {
871   my ($self) = @_;
872
873   # add_items adds items to an order with no items for saving, but they cannot
874   # be retrieved via items until the order is saved. Adding empty items to new
875   # order here solves this problem.
876   my $order;
877   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
878   $order ||= SL::DB::Order->new(orderitems => []);
879
880   my $form_orderitems = delete $::form->{order}->{orderitems};
881   $order->assign_attributes(%{$::form->{order}});
882
883   # remove deleted items
884   $self->item_ids_to_delete([]);
885   foreach my $idx (reverse 0..$#{$order->orderitems}) {
886     my $item = $order->orderitems->[$idx];
887     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
888       splice @{$order->orderitems}, $idx, 1;
889       push @{$self->item_ids_to_delete}, $item->id;
890     }
891   }
892
893   my @items;
894   my $pos = 1;
895   foreach my $form_attr (@{$form_orderitems}) {
896     my $item = _make_item($order, $form_attr);
897     $item->position($pos);
898     push @items, $item;
899     $pos++;
900   }
901   $order->add_items(grep {!$_->id} @items);
902
903   return $order;
904 }
905
906 # create or update items from form
907 #
908 # Make item objects from form values. For items already existing read from db.
909 # Create a new item else. And assign attributes.
910 sub _make_item {
911   my ($record, $attr) = @_;
912
913   my $item;
914   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
915
916   my $is_new = !$item;
917
918   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
919   # they cannot be retrieved via custom_variables until the order/orderitem is
920   # saved. Adding empty custom_variables to new orderitem here solves this problem.
921   $item ||= SL::DB::OrderItem->new(custom_variables => []);
922
923   $item->assign_attributes(%$attr);
924   $item->longdescription($item->part->notes)   if $is_new && !defined $attr->{longdescription};
925   $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
926   $item->lastcost($item->part->lastcost)       if $is_new && !defined $attr->{lastcost_as_number};
927
928   return $item;
929 }
930
931 # create a new item
932 #
933 # This is used to add one item
934 sub _new_item {
935   my ($record, $attr) = @_;
936
937   my $item = SL::DB::OrderItem->new;
938   $item->assign_attributes(%$attr);
939
940   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
941   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
942
943   $item->unit($part->unit) if !$item->unit;
944
945   my $price_src;
946   if ( $part->is_assortment ) {
947     # add assortment items with price 0, as the components carry the price
948     $price_src = $price_source->price_from_source("");
949     $price_src->price(0);
950   } elsif ($item->sellprice) {
951     $price_src = $price_source->price_from_source("");
952     $price_src->price($item->sellprice);
953   } else {
954     $price_src = $price_source->best_price
955            ? $price_source->best_price
956            : $price_source->price_from_source("");
957     $price_src->price(0) if !$price_source->best_price;
958   }
959
960   my $discount_src;
961   if ($item->discount) {
962     $discount_src = $price_source->discount_from_source("");
963     $discount_src->discount($item->discount);
964   } else {
965     $discount_src = $price_source->best_discount
966                   ? $price_source->best_discount
967                   : $price_source->discount_from_source("");
968     $discount_src->discount(0) if !$price_source->best_discount;
969   }
970
971   my %new_attr;
972   $new_attr{part}                   = $part;
973   $new_attr{description}            = $part->description     if ! $item->description;
974   $new_attr{qty}                    = 1.0                    if ! $item->qty;
975   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
976   $new_attr{sellprice}              = $price_src->price;
977   $new_attr{discount}               = $discount_src->discount;
978   $new_attr{active_price_source}    = $price_src;
979   $new_attr{active_discount_source} = $discount_src;
980   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
981   $new_attr{project_id}             = $record->globalproject_id;
982   $new_attr{lastcost}               = $part->lastcost;
983
984   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
985   # they cannot be retrieved via custom_variables until the order/orderitem is
986   # saved. Adding empty custom_variables to new orderitem here solves this problem.
987   $new_attr{custom_variables} = [];
988
989   $item->assign_attributes(%new_attr);
990
991   return $item;
992 }
993
994 # recalculate prices and taxes
995 #
996 # Using the PriceTaxCalculator. Store linetotals in the item objects.
997 sub _recalc {
998   my ($self) = @_;
999
1000   # bb: todo: currency later
1001   $self->order->currency_id($::instance_conf->get_currency_id());
1002
1003   my %pat = $self->order->calculate_prices_and_taxes();
1004   $self->{taxes} = [];
1005   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1006     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1007
1008     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1009     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
1010                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1011                                 tax       => $tax });
1012   }
1013
1014   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1015 }
1016
1017 # get data for saving, printing, ..., that is not changed in the form
1018 #
1019 # Only cvars for now.
1020 sub _get_unalterable_data {
1021   my ($self) = @_;
1022
1023   foreach my $item (@{ $self->order->items }) {
1024     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1025     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1026     foreach my $var (@{ $item->cvars_by_config }) {
1027       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1028     }
1029     $item->parse_custom_variable_values;
1030   }
1031 }
1032
1033 # delete the order
1034 #
1035 # And remove related files in the spool directory
1036 sub _delete {
1037   my ($self) = @_;
1038
1039   my $errors = [];
1040   my $db     = $self->order->db;
1041
1042   $db->with_transaction(
1043     sub {
1044       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1045       $self->order->delete;
1046       my $spool = $::lx_office_conf{paths}->{spool};
1047       unlink map { "$spool/$_" } @spoolfiles if $spool;
1048
1049       1;
1050   }) || push(@{$errors}, $db->error);
1051
1052   return $errors;
1053 }
1054
1055 # save the order
1056 #
1057 # And delete items that are deleted in the form.
1058 sub _save {
1059   my ($self) = @_;
1060
1061   my $errors = [];
1062   my $db     = $self->order->db;
1063
1064   $db->with_transaction(sub {
1065     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1066     $self->order->save(cascade => 1);
1067   }) || push(@{$errors}, $db->error);
1068
1069   return $errors;
1070 }
1071
1072
1073 sub _pre_render {
1074   my ($self) = @_;
1075
1076   $self->{all_taxzones}        = SL::DB::Manager::TaxZone->get_all_sorted();
1077   $self->{all_departments}     = SL::DB::Manager::Department->get_all_sorted();
1078   $self->{all_employees}       = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1079                                                                                        deleted => 0 ] ],
1080                                                                     sort_by => 'name');
1081   $self->{all_salesmen}        = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1082                                                                                        deleted => 0 ] ],
1083                                                                     sort_by => 'name');
1084   $self->{all_projects}        = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1085                                                                                       active => 1 ] ],
1086                                                                    sort_by => 'projectnumber');
1087   $self->{all_payment_terms}   = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1088                                                                                                  obsolete => 0 ] ]);
1089
1090   $self->{all_delivery_terms}  = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1091
1092   $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1093
1094   my $print_form = Form->new('');
1095   $print_form->{type}      = $self->type;
1096   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
1097   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1098   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
1099     form => $print_form,
1100     options => {dialog_name_prefix => 'print_options.',
1101                 show_headers       => 1,
1102                 no_queue           => 1,
1103                 no_postscript      => 1,
1104                 no_opendocument    => 1,
1105                 no_html            => 1},
1106   );
1107
1108   foreach my $item (@{$self->order->orderitems}) {
1109     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1110     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1111     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1112   }
1113
1114   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1115     my $webdav = SL::Webdav->new(
1116       type     => $self->type,
1117       number   => $self->order->ordnumber,
1118     );
1119     my @all_objects = $webdav->get_all_objects;
1120     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1121                                                     type => t8('File'),
1122                                                     link => File::Spec->catfile($_->full_filedescriptor),
1123                                                 } } @all_objects;
1124   }
1125
1126   $::request->{layout}->use_javascript("${_}.js")  for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery);
1127   $self->_setup_edit_action_bar;
1128 }
1129
1130 sub _setup_edit_action_bar {
1131   my ($self, %params) = @_;
1132
1133   my $deletion_allowed = (($self->cv eq 'customer') && $::instance_conf->get_sales_order_show_delete)
1134                       || (($self->cv eq 'vendor')   && $::instance_conf->get_purchase_order_show_delete);
1135
1136   for my $bar ($::request->layout->get('actionbar')) {
1137     $bar->add(
1138       combobox => [
1139         action => [
1140           t8('Save'),
1141           call      => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1142           accesskey => 'enter',
1143         ],
1144         action => [
1145           t8('Save and Delivery Order'),
1146           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1147         ],
1148         action => [
1149           t8('Save and Invoice'),
1150           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1151         ],
1152
1153       ], # end of combobox "Save"
1154
1155       combobox => [
1156         action => [
1157           t8('Export'),
1158         ],
1159         action => [
1160           t8('Print'),
1161           call => [ 'kivi.Order.show_print_options' ],
1162         ],
1163         action => [
1164           t8('E-mail'),
1165           call => [ 'kivi.Order.email' ],
1166         ],
1167         action => [
1168           t8('Download attachments of all parts'),
1169           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1170           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1171           only_if  => $::instance_conf->get_doc_storage,
1172         ],
1173       ], # end of combobox "Export"
1174
1175       action => [
1176         t8('Delete'),
1177         call     => [ 'kivi.Order.delete_order' ],
1178         confirm  => $::locale->text('Do you really want to delete this object?'),
1179         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1180         only_if  => $deletion_allowed,
1181       ],
1182     );
1183   }
1184 }
1185
1186 sub _create_pdf {
1187   my ($order, $pdf_ref, $params) = @_;
1188
1189   my @errors = ();
1190
1191   my $print_form = Form->new('');
1192   $print_form->{type}        = $order->type;
1193   $print_form->{formname}    = $params->{formname} || $order->type;
1194   $print_form->{format}      = $params->{format}   || 'pdf';
1195   $print_form->{media}       = $params->{media}    || 'file';
1196   $print_form->{groupitems}  = $params->{groupitems};
1197   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1198   $print_form->{language}    = $params->{language}->template_code if $print_form->{language};
1199   $print_form->{language_id} = $params->{language}->id            if $print_form->{language};
1200
1201   $order->flatten_to_form($print_form, format_amounts => 1);
1202
1203   # search for the template
1204   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1205     name        => $print_form->{formname},
1206     email       => $print_form->{media} eq 'email',
1207     language    => $params->{language},
1208     printer_id  => $print_form->{printer_id},  # todo
1209   );
1210
1211   if (!defined $template_file) {
1212     push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
1213   }
1214
1215   return @errors if scalar @errors;
1216
1217   $print_form->throw_on_error(sub {
1218     eval {
1219       $print_form->prepare_for_printing;
1220
1221       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1222         template  => $template_file,
1223         variables => $print_form,
1224         variable_content_types => {
1225           longdescription => 'html',
1226           partnotes       => 'html',
1227           notes           => 'html',
1228         },
1229       );
1230       1;
1231     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1232   });
1233
1234   return @errors;
1235 }
1236
1237 sub _sales_order_type {
1238   'sales_order';
1239 }
1240
1241 sub _purchase_order_type {
1242   'purchase_order';
1243 }
1244
1245 1;
1246
1247 __END__
1248
1249 =encoding utf-8
1250
1251 =head1 NAME
1252
1253 SL::Controller::Order - controller for orders
1254
1255 =head1 SYNOPSIS
1256
1257 This is a new form to enter orders, completely rewritten with the use
1258 of controller and java script techniques.
1259
1260 The aim is to provide the user a better expirience and a faster flow
1261 of work. Also the code should be more readable, more reliable and
1262 better to maintain.
1263
1264 =head2 Key Features
1265
1266 =over 4
1267
1268 =item *
1269
1270 One input row, so that input happens every time at the same place.
1271
1272 =item *
1273
1274 Use of pickers where possible.
1275
1276 =item *
1277
1278 Possibility to enter more than one item at once.
1279
1280 =item *
1281
1282 Save order only on "save" (and "save and delivery order"-workflow). No
1283 hidden save on "print" or "email".
1284
1285 =item *
1286
1287 Item list in a scrollable area, so that the workflow buttons stay at
1288 the bottom.
1289
1290 =item *
1291
1292 Reordering item rows with drag and drop is possible. Sorting item rows is
1293 possible (by partnumber, description, qty, sellprice and discount for now).
1294
1295 =item *
1296
1297 No C<update> is necessary. All entries and calculations are managed
1298 with ajax-calls and the page does only reload on C<save>.
1299
1300 =item *
1301
1302 User can see changes immediately, because of the use of java script
1303 and ajax.
1304
1305 =back
1306
1307 =head1 CODE
1308
1309 =head2 Layout
1310
1311 =over 4
1312
1313 =item * C<SL/Controller/Order.pm>
1314
1315 the controller
1316
1317 =item * C<template/webpages/order/form.html>
1318
1319 main form
1320
1321 =item * C<template/webpages/order/tabs/basic_data.html>
1322
1323 Main tab for basic_data.
1324
1325 This is the only tab here for now. "linked records" and "webdav" tabs are
1326 reused from generic code.
1327
1328 =over 4
1329
1330 =item * C<template/webpages/order/tabs/_item_input.html>
1331
1332 The input line for items
1333
1334 =item * C<template/webpages/order/tabs/_row.html>
1335
1336 One row for already entered items
1337
1338 =item * C<template/webpages/order/tabs/_tax_row.html>
1339
1340 Displaying tax information
1341
1342 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1343
1344 Dialog for entering more than one item at once
1345
1346 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1347
1348 Results for the filter in the multi items dialog
1349
1350 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1351
1352 Dialog for selecting price and discount sources
1353
1354 =item * C<template/webpages/order/tabs/_email_dialog.html>
1355
1356 Email dialog
1357
1358 =back
1359
1360 =item * C<js/kivi.Order.js>
1361
1362 java script functions
1363
1364 =back
1365
1366 =head1 TODO
1367
1368 =over 4
1369
1370 =item * testing
1371
1372 =item * currency
1373
1374 =item * customer/vendor details ('D'-button)
1375
1376 =item * credit limit
1377
1378 =item * more workflows (save as new, quotation, purchase order)
1379
1380 =item * price sources: little symbols showing better price / better discount
1381
1382 =item * select units in input row?
1383
1384 =item * custom shipto address
1385
1386 =item * periodic invoices
1387
1388 =item * language / part translations
1389
1390 =item * access rights
1391
1392 =item * display weights
1393
1394 =item * history
1395
1396 =item * mtime check
1397
1398 =item * optional client/user behaviour
1399
1400 (transactions has to be set - department has to be set -
1401  force project if enabled in client config - transport cost reminder)
1402
1403 =back
1404
1405 =head1 KNOWN BUGS AND CAVEATS
1406
1407 =over 4
1408
1409 =item *
1410
1411 Customer discount is not displayed as a valid discount in price source popup
1412 (this might be a bug in price sources)
1413
1414 (I cannot reproduce this (Bernd))
1415
1416 =item *
1417
1418 No indication that <shift>-up/down expands/collapses second row.
1419
1420 =item *
1421
1422 Inline creation of parts is not currently supported
1423
1424 =item *
1425
1426 Table header is not sticky in the scrolling area.
1427
1428 =item *
1429
1430 Sorting does not include C<position>, neither does reordering.
1431
1432 This behavior was implemented intentionally. But we can discuss, which behavior
1433 should be implemented.
1434
1435 =item *
1436
1437 C<show_multi_items_dialog> does not use the currently inserted string for
1438 filtering.
1439
1440 =back
1441
1442 =head1 To discuss / Nice to have
1443
1444 =over 4
1445
1446 =item *
1447
1448 How to expand/collapse second row. Now it can be done clicking the icon or
1449 <shift>-up/down.
1450
1451 =item *
1452
1453 Possibility to change longdescription in input row?
1454
1455 =item *
1456
1457 Possibility to select PriceSources in input row?
1458
1459 =item *
1460
1461 This controller uses a (changed) copy of the template for the PriceSource
1462 dialog. Maybe there could be used one code source.
1463
1464 =item *
1465
1466 Rounding-differences between this controller (PriceTaxCalculator) and the old
1467 form. This is not only a problem here, but also in all parts using the PTC.
1468 There exists a ticket and a patch. This patch should be testet.
1469
1470 =item *
1471
1472 An indicator, if the actual inputs are saved (like in an
1473 editor or on text processing application).
1474
1475 =item *
1476
1477 A warning when leaving the page without saveing unchanged inputs.
1478
1479 =item *
1480
1481 Workflows for delivery order and invoice are in the menu "Save", because the
1482 order is saved before opening the new document form. Nevertheless perhaps these
1483 workflow buttons should be put under "Workflows".
1484
1485
1486 =back
1487
1488 =head1 AUTHOR
1489
1490 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1491
1492 =cut