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