ac619d5bdc3de6cf93a97c16c297c5c577d0d19a
[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 hidden_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(any 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 # open the periodic invoices config dialog
352 #
353 # If there are values in the form (i.e. dialog was opened before),
354 # then use this values. Create new ones, else.
355 sub action_show_periodic_invoices_config_dialog {
356   my ($self) = @_;
357
358   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
359   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
360   $config  ||= SL::DB::PeriodicInvoicesConfig->new(periodicity             => 'm',
361                                                    order_value_periodicity => 'p', # = same as periodicity
362                                                    start_date_as_date      => $::form->{transdate} || $::form->current_date,
363                                                    extend_automatically_by => 12,
364                                                    active                  => 1,
365                                                    email_subject           => GenericTranslations->get(
366                                                                                 language_id      => $::form->{language_id},
367                                                                                 translation_type =>"preset_text_periodic_invoices_email_subject"),
368                                                    email_body              => GenericTranslations->get(
369                                                                                 language_id      => $::form->{language_id},
370                                                                                 translation_type =>"preset_text_periodic_invoices_email_body"),
371   );
372   $config->periodicity('m')             if none { $_ eq $config->periodicity             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
373   $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
374
375   $::form->get_lists(printers => "ALL_PRINTERS",
376                      charts   => { key       => 'ALL_CHARTS',
377                                    transdate => 'current_date' });
378
379   $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
380
381   if ($::form->{customer_id}) {
382     $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
383   }
384
385   $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
386                 popup_dialog             => 1,
387                 popup_js_close_function  => 'kivi.Order.close_periodic_invoices_config_dialog()',
388                 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
389                 config                   => $config,
390                 %$::form);
391 }
392
393 # assign the values of the periodic invoices config dialog
394 # as yaml in the hidden tag and set the status.
395 sub action_assign_periodic_invoices_config {
396   my ($self) = @_;
397
398   $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
399
400   my $config = { active                  => $::form->{active}     ? 1 : 0,
401                  terminated              => $::form->{terminated} ? 1 : 0,
402                  direct_debit            => $::form->{direct_debit} ? 1 : 0,
403                  periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
404                  order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
405                  start_date_as_date      => $::form->{start_date_as_date},
406                  end_date_as_date        => $::form->{end_date_as_date},
407                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
408                  print                   => $::form->{print} ? 1 : 0,
409                  printer_id              => $::form->{print} ? $::form->{printer_id} * 1 : undef,
410                  copies                  => $::form->{copies} * 1 ? $::form->{copies} : 1,
411                  extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
412                  ar_chart_id             => $::form->{ar_chart_id} * 1,
413                  send_email                 => $::form->{send_email} ? 1 : 0,
414                  email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
415                  email_recipient_address    => $::form->{email_recipient_address},
416                  email_sender               => $::form->{email_sender},
417                  email_subject              => $::form->{email_subject},
418                  email_body                 => $::form->{email_body},
419                };
420
421   my $periodic_invoices_config = YAML::Dump($config);
422
423   my $status = $self->_get_periodic_invoices_status($config);
424
425   $self->js
426     ->remove('#order_periodic_invoices_config')
427     ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
428     ->run('kivi.Order.close_periodic_invoices_config_dialog')
429     ->html('#periodic_invoices_status', $status)
430     ->flash('info', t8('The periodic invoices config has been assigned.'))
431     ->render($self);
432 }
433
434 sub action_get_has_active_periodic_invoices {
435   my ($self) = @_;
436
437   my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
438   $config  ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
439
440   my $has_active_periodic_invoices =
441        $self->type eq _sales_order_type()
442     && $config
443     && $config->active
444     && (!$config->end_date || ($config->end_date > DateTime->today_local))
445     && $config->get_previous_billed_period_start_date;
446
447   $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
448 }
449
450 # save the order and redirect to the frontend subroutine for a new
451 # delivery order
452 sub action_save_and_delivery_order {
453   my ($self) = @_;
454
455   my $errors = $self->_save();
456
457   if (scalar @{ $errors }) {
458     $self->js->flash('error', $_) foreach @{ $errors };
459     return $self->js->render();
460   }
461   flash_later('info', $::locale->text('The order has been saved'));
462
463   my @redirect_params = (
464     controller => 'oe.pl',
465     action     => 'oe_delivery_order_from_order',
466     id         => $self->order->id,
467   );
468
469   $self->redirect_to(@redirect_params);
470 }
471
472 # save the order and redirect to the frontend subroutine for a new
473 # invoice
474 sub action_save_and_invoice {
475   my ($self) = @_;
476
477   my $errors = $self->_save();
478
479   if (scalar @{ $errors }) {
480     $self->js->flash('error', $_) foreach @{ $errors };
481     return $self->js->render();
482   }
483   flash_later('info', $::locale->text('The order has been saved'));
484
485   my @redirect_params = (
486     controller => 'oe.pl',
487     action     => 'oe_invoice_from_order',
488     id         => $self->order->id,
489   );
490
491   $self->redirect_to(@redirect_params);
492 }
493
494 # set form elements in respect to a changed customer or vendor
495 #
496 # This action is called on an change of the customer/vendor picker.
497 sub action_customer_vendor_changed {
498   my ($self) = @_;
499
500   my $cv_method = $self->cv;
501
502   if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
503     $self->js->show('#cp_row');
504   } else {
505     $self->js->hide('#cp_row');
506   }
507
508   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
509     $self->js->show('#shipto_row');
510   } else {
511     $self->js->hide('#shipto_row');
512   }
513
514   $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
515
516   if ($self->order->is_sales) {
517     $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
518                               ? $self->order->$cv_method->taxincluded_checked
519                               : $::myconfig{taxincluded_checked});
520     $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
521   }
522
523   $self->order->payment_id($self->order->$cv_method->payment_id);
524   $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
525
526   $self->_recalc();
527
528   $self->js
529     ->replaceWith('#order_cp_id',            $self->build_contact_select)
530     ->replaceWith('#order_shipto_id',        $self->build_shipto_select)
531     ->val(        '#order_taxzone_id',       $self->order->taxzone_id)
532     ->val(        '#order_taxincluded',      $self->order->taxincluded)
533     ->val(        '#order_payment_id',       $self->order->payment_id)
534     ->val(        '#order_delivery_term_id', $self->order->delivery_term_id)
535     ->val(        '#order_intnotes',         $self->order->$cv_method->notes)
536     ->focus(      '#order_' . $self->cv . '_id');
537
538   $self->_js_redisplay_amounts_and_taxes;
539   $self->js->render();
540 }
541
542 # called if a unit in an existing item row is changed
543 sub action_unit_changed {
544   my ($self) = @_;
545
546   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
547   my $item = $self->order->items_sorted->[$idx];
548
549   my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
550   $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
551
552   $self->_recalc();
553
554   $self->js
555     ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
556   $self->_js_redisplay_line_values;
557   $self->_js_redisplay_amounts_and_taxes;
558   $self->js->render();
559 }
560
561 # add an item row for a new item entered in the input row
562 sub action_add_item {
563   my ($self) = @_;
564
565   my $form_attr = $::form->{add_item};
566
567   return unless $form_attr->{parts_id};
568
569   my $item = _new_item($self->order, $form_attr);
570
571   $self->order->add_items($item);
572
573   $self->_recalc();
574
575   my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
576   my $row_as_html = $self->p->render('order/tabs/_row',
577                                      ITEM              => $item,
578                                      ID                => $item_id,
579                                      TYPE              => $self->type,
580                                      ALL_PRICE_FACTORS => $self->all_price_factors
581   );
582
583   $self->js
584     ->append('#row_table_id', $row_as_html);
585
586   if ( $item->part->is_assortment ) {
587     $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
588     foreach my $assortment_item ( @{$item->part->assortment_items} ) {
589       my $attr = { parts_id => $assortment_item->parts_id,
590                    qty      => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
591                    unit     => $assortment_item->unit,
592                    description => $assortment_item->part->description,
593                  };
594       my $item = _new_item($self->order, $attr);
595
596       # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
597       $item->discount(1) unless $assortment_item->charge;
598
599       $self->order->add_items( $item );
600       $self->_recalc();
601       my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
602       my $row_as_html = $self->p->render('order/tabs/_row',
603                                          ITEM              => $item,
604                                          ID                => $item_id,
605                                          TYPE              => $self->type,
606                                          ALL_PRICE_FACTORS => $self->all_price_factors
607       );
608       $self->js
609         ->append('#row_table_id', $row_as_html);
610     };
611   };
612
613   $self->js
614     ->val('.add_item_input', '')
615     ->run('kivi.Order.init_row_handlers')
616     ->run('kivi.Order.row_table_scroll_down')
617     ->run('kivi.Order.renumber_positions')
618     ->focus('#add_item_parts_id_name');
619
620   $self->_js_redisplay_amounts_and_taxes;
621   $self->js->render();
622 }
623
624 # open the dialog for entering multiple items at once
625 sub action_show_multi_items_dialog {
626   require SL::DB::PartsGroup;
627   $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
628                 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
629 }
630
631 # update the filter results in the multi item dialog
632 sub action_multi_items_update_result {
633   my $max_count = 100;
634
635   $::form->{multi_items}->{filter}->{obsolete} = 0;
636
637   my $count = $_[0]->multi_items_models->count;
638
639   if ($count == 0) {
640     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
641     $_[0]->render($text, { layout => 0 });
642   } elsif ($count > $max_count) {
643     my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
644     $_[0]->render($text, { layout => 0 });
645   } else {
646     my $multi_items = $_[0]->multi_items_models->get;
647     $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
648                   multi_items => $multi_items);
649   }
650 }
651
652 # add item rows for multiple items at once
653 sub action_add_multi_items {
654   my ($self) = @_;
655
656   my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
657   return $self->js->render() unless scalar @form_attr;
658
659   my @items;
660   foreach my $attr (@form_attr) {
661     my $item = _new_item($self->order, $attr);
662     push @items, $item;
663     if ( $item->part->is_assortment ) {
664       foreach my $assortment_item ( @{$item->part->assortment_items} ) {
665         my $attr = { parts_id => $assortment_item->parts_id,
666                      qty      => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
667                      unit     => $assortment_item->unit,
668                      description => $assortment_item->part->description,
669                    };
670         my $item = _new_item($self->order, $attr);
671
672         # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
673         $item->discount(1) unless $assortment_item->charge;
674         push @items, $item;
675       }
676     }
677   }
678   $self->order->add_items(@items);
679
680   $self->_recalc();
681
682   foreach my $item (@items) {
683     my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
684     my $row_as_html = $self->p->render('order/tabs/_row',
685                                        ITEM              => $item,
686                                        ID                => $item_id,
687                                        TYPE              => $self->type,
688                                        ALL_PRICE_FACTORS => $self->all_price_factors
689     );
690
691     $self->js->append('#row_table_id', $row_as_html);
692   }
693
694   $self->js
695     ->run('kivi.Order.close_multi_items_dialog')
696     ->run('kivi.Order.init_row_handlers')
697     ->run('kivi.Order.row_table_scroll_down')
698     ->run('kivi.Order.renumber_positions')
699     ->focus('#add_item_parts_id_name');
700
701   $self->_js_redisplay_amounts_and_taxes;
702   $self->js->render();
703 }
704
705 # recalculate all linetotals, amounts and taxes and redisplay them
706 sub action_recalc_amounts_and_taxes {
707   my ($self) = @_;
708
709   $self->_recalc();
710
711   $self->_js_redisplay_line_values;
712   $self->_js_redisplay_amounts_and_taxes;
713   $self->js->render();
714 }
715
716 # redisplay item rows if they are sorted by an attribute
717 sub action_reorder_items {
718   my ($self) = @_;
719
720   my %sort_keys = (
721     partnumber  => sub { $_[0]->part->partnumber },
722     description => sub { $_[0]->description },
723     qty         => sub { $_[0]->qty },
724     sellprice   => sub { $_[0]->sellprice },
725     discount    => sub { $_[0]->discount },
726   );
727
728   my $method = $sort_keys{$::form->{order_by}};
729   my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
730   if ($::form->{sort_dir}) {
731     @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
732   } else {
733     @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
734   }
735   $self->js
736     ->run('kivi.Order.redisplay_items', \@to_sort)
737     ->render;
738 }
739
740 # show the popup to choose a price/discount source
741 sub action_price_popup {
742   my ($self) = @_;
743
744   my $idx  = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
745   my $item = $self->order->items_sorted->[$idx];
746
747   $self->render_price_dialog($item);
748 }
749
750 # get the longdescription for an item if the dialog to enter/change the
751 # longdescription was opened and the longdescription is empty
752 #
753 # If this item is new, get the longdescription from Part.
754 # Otherwise get it from OrderItem.
755 sub action_get_item_longdescription {
756   my $longdescription;
757
758   if ($::form->{item_id}) {
759     $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
760   } elsif ($::form->{parts_id}) {
761     $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
762   }
763   $_[0]->render(\ $longdescription, { type => 'text' });
764 }
765
766 # load the second row for one or more items
767 #
768 # This action gets the html code for all items second rows by rendering a template for
769 # the second row and sets the html code via client js.
770 sub action_load_second_rows {
771   my ($self) = @_;
772
773   $self->_recalc() if $self->order->is_sales; # for margin calculation
774
775   foreach my $item_id (@{ $::form->{item_ids} }) {
776     my $idx  = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
777     my $item = $self->order->items_sorted->[$idx];
778
779     $self->_js_load_second_row($item, $item_id, 0);
780   }
781
782   $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
783
784   $self->js->render();
785 }
786
787 sub _js_load_second_row {
788   my ($self, $item, $item_id, $do_parse) = @_;
789
790   if ($do_parse) {
791     # Parse values from form (they are formated while rendering (template)).
792     # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
793     # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
794     foreach my $var (@{ $item->cvars_by_config }) {
795       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
796     }
797     $item->parse_custom_variable_values;
798   }
799
800   my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
801
802   $self->js
803     ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
804     ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
805 }
806
807 sub _js_redisplay_line_values {
808   my ($self) = @_;
809
810   my $is_sales = $self->order->is_sales;
811
812   # sales orders with margins
813   my @data;
814   if ($is_sales) {
815     @data = map {
816       [
817        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
818        $::form->format_amount(\%::myconfig, $_->{marge_total},   2, 0),
819        $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
820       ]} @{ $self->order->items_sorted };
821   } else {
822     @data = map {
823       [
824        $::form->format_amount(\%::myconfig, $_->{linetotal},     2, 0),
825       ]} @{ $self->order->items_sorted };
826   }
827
828   $self->js
829     ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
830 }
831
832 sub _js_redisplay_amounts_and_taxes {
833   my ($self) = @_;
834
835   if (scalar @{ $self->{taxes} }) {
836     $self->js->show('#taxincluded_row_id');
837   } else {
838     $self->js->hide('#taxincluded_row_id');
839   }
840
841   if ($self->order->taxincluded) {
842     $self->js->hide('#subtotal_row_id');
843   } else {
844     $self->js->show('#subtotal_row_id');
845   }
846
847   $self->js
848     ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
849     ->html('#amount_id',    $::form->format_amount(\%::myconfig, $self->order->amount,    -2))
850     ->remove('.tax_row')
851     ->insertBefore($self->build_tax_rows, '#amount_row_id');
852 }
853
854 #
855 # helpers
856 #
857
858 sub init_valid_types {
859   [ _sales_order_type(), _purchase_order_type() ];
860 }
861
862 sub init_type {
863   my ($self) = @_;
864
865   if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
866     die "Not a valid type for order";
867   }
868
869   $self->type($::form->{type});
870 }
871
872 sub init_cv {
873   my ($self) = @_;
874
875   my $cv = $self->type eq _sales_order_type()    ? 'customer'
876          : $self->type eq _purchase_order_type() ? 'vendor'
877          : die "Not a valid type for order";
878
879   return $cv;
880 }
881
882 sub init_p {
883   SL::Presenter->get;
884 }
885
886 sub init_order {
887   $_[0]->_make_order;
888 }
889
890 # model used to filter/display the parts in the multi-items dialog
891 sub init_multi_items_models {
892   SL::Controller::Helper::GetModels->new(
893     controller     => $_[0],
894     model          => 'Part',
895     with_objects   => [ qw(unit_obj) ],
896     disable_plugin => 'paginated',
897     source         => $::form->{multi_items},
898     sorted         => {
899       _default    => {
900         by  => 'partnumber',
901         dir => 1,
902       },
903       partnumber  => t8('Partnumber'),
904       description => t8('Description')}
905   );
906 }
907
908 sub init_all_price_factors {
909   SL::DB::Manager::PriceFactor->get_all;
910 }
911
912 sub _check_auth {
913   my ($self) = @_;
914
915   my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
916
917   my $right   = $right_for->{ $self->type };
918   $right    ||= 'DOES_NOT_EXIST';
919
920   $::auth->assert($right);
921 }
922
923 # build the selection box for contacts
924 #
925 # Needed, if customer/vendor changed.
926 sub build_contact_select {
927   my ($self) = @_;
928
929   select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
930     value_key  => 'cp_id',
931     title_key  => 'full_name_dep',
932     default    => $self->order->cp_id,
933     with_empty => 1,
934     style      => 'width: 300px',
935   );
936 }
937
938 # build the selection box for shiptos
939 #
940 # Needed, if customer/vendor changed.
941 sub build_shipto_select {
942   my ($self) = @_;
943
944   select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
945     value_key  => 'shipto_id',
946     title_key  => 'displayable_id',
947     default    => $self->order->shipto_id,
948     with_empty => 1,
949     style      => 'width: 300px',
950   );
951 }
952
953 # build the rows for displaying taxes
954 #
955 # Called if amounts where recalculated and redisplayed.
956 sub build_tax_rows {
957   my ($self) = @_;
958
959   my $rows_as_html;
960   foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
961     $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
962   }
963   return $rows_as_html;
964 }
965
966
967 sub render_price_dialog {
968   my ($self, $record_item) = @_;
969
970   my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
971
972   $self->js
973     ->run(
974       'kivi.io.price_chooser_dialog',
975       t8('Available Prices'),
976       $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
977     )
978     ->reinit_widgets;
979
980 #   if (@errors) {
981 #     $self->js->text('#dialog_flash_error_content', join ' ', @errors);
982 #     $self->js->show('#dialog_flash_error');
983 #   }
984
985   $self->js->render;
986 }
987
988 sub _load_order {
989   my ($self) = @_;
990
991   return if !$::form->{id};
992
993   $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
994 }
995
996 # load or create a new order object
997 #
998 # And assign changes from the form to this object.
999 # If the order is loaded from db, check if items are deleted in the form,
1000 # remove them form the object and collect them for removing from db on saving.
1001 # Then create/update items from form (via _make_item) and add them.
1002 sub _make_order {
1003   my ($self) = @_;
1004
1005   # add_items adds items to an order with no items for saving, but they cannot
1006   # be retrieved via items until the order is saved. Adding empty items to new
1007   # order here solves this problem.
1008   my $order;
1009   $order   = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1010   $order ||= SL::DB::Order->new(orderitems => []);
1011
1012   my $form_orderitems               = delete $::form->{order}->{orderitems};
1013   my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1014
1015   $order->assign_attributes(%{$::form->{order}});
1016
1017   my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1018   $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1019
1020   # remove deleted items
1021   $self->item_ids_to_delete([]);
1022   foreach my $idx (reverse 0..$#{$order->orderitems}) {
1023     my $item = $order->orderitems->[$idx];
1024     if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1025       splice @{$order->orderitems}, $idx, 1;
1026       push @{$self->item_ids_to_delete}, $item->id;
1027     }
1028   }
1029
1030   my @items;
1031   my $pos = 1;
1032   foreach my $form_attr (@{$form_orderitems}) {
1033     my $item = _make_item($order, $form_attr);
1034     $item->position($pos);
1035     push @items, $item;
1036     $pos++;
1037   }
1038   $order->add_items(grep {!$_->id} @items);
1039
1040   return $order;
1041 }
1042
1043 # create or update items from form
1044 #
1045 # Make item objects from form values. For items already existing read from db.
1046 # Create a new item else. And assign attributes.
1047 sub _make_item {
1048   my ($record, $attr) = @_;
1049
1050   my $item;
1051   $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1052
1053   my $is_new = !$item;
1054
1055   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1056   # they cannot be retrieved via custom_variables until the order/orderitem is
1057   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1058   $item ||= SL::DB::OrderItem->new(custom_variables => []);
1059
1060   $item->assign_attributes(%$attr);
1061   $item->longdescription($item->part->notes)   if $is_new && !defined $attr->{longdescription};
1062   $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1063   $item->lastcost($item->part->lastcost)       if $is_new && !defined $attr->{lastcost_as_number};
1064
1065   return $item;
1066 }
1067
1068 # create a new item
1069 #
1070 # This is used to add one item
1071 sub _new_item {
1072   my ($record, $attr) = @_;
1073
1074   my $item = SL::DB::OrderItem->new;
1075   $item->assign_attributes(%$attr);
1076
1077   my $part         = SL::DB::Part->new(id => $attr->{parts_id})->load;
1078   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1079
1080   $item->unit($part->unit) if !$item->unit;
1081
1082   my $price_src;
1083   if ( $part->is_assortment ) {
1084     # add assortment items with price 0, as the components carry the price
1085     $price_src = $price_source->price_from_source("");
1086     $price_src->price(0);
1087   } elsif ($item->sellprice) {
1088     $price_src = $price_source->price_from_source("");
1089     $price_src->price($item->sellprice);
1090   } else {
1091     $price_src = $price_source->best_price
1092            ? $price_source->best_price
1093            : $price_source->price_from_source("");
1094     $price_src->price(0) if !$price_source->best_price;
1095   }
1096
1097   my $discount_src;
1098   if ($item->discount) {
1099     $discount_src = $price_source->discount_from_source("");
1100     $discount_src->discount($item->discount);
1101   } else {
1102     $discount_src = $price_source->best_discount
1103                   ? $price_source->best_discount
1104                   : $price_source->discount_from_source("");
1105     $discount_src->discount(0) if !$price_source->best_discount;
1106   }
1107
1108   my %new_attr;
1109   $new_attr{part}                   = $part;
1110   $new_attr{description}            = $part->description     if ! $item->description;
1111   $new_attr{qty}                    = 1.0                    if ! $item->qty;
1112   $new_attr{price_factor_id}        = $part->price_factor_id if ! $item->price_factor_id;
1113   $new_attr{sellprice}              = $price_src->price;
1114   $new_attr{discount}               = $discount_src->discount;
1115   $new_attr{active_price_source}    = $price_src;
1116   $new_attr{active_discount_source} = $discount_src;
1117   $new_attr{longdescription}        = $part->notes           if ! defined $attr->{longdescription};
1118   $new_attr{project_id}             = $record->globalproject_id;
1119   $new_attr{lastcost}               = $part->lastcost;
1120
1121   # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1122   # they cannot be retrieved via custom_variables until the order/orderitem is
1123   # saved. Adding empty custom_variables to new orderitem here solves this problem.
1124   $new_attr{custom_variables} = [];
1125
1126   $item->assign_attributes(%new_attr);
1127
1128   return $item;
1129 }
1130
1131 # recalculate prices and taxes
1132 #
1133 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1134 sub _recalc {
1135   my ($self) = @_;
1136
1137   # bb: todo: currency later
1138   $self->order->currency_id($::instance_conf->get_currency_id());
1139
1140   my %pat = $self->order->calculate_prices_and_taxes();
1141   $self->{taxes} = [];
1142   foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1143     my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1144
1145     my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1146     push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
1147                                 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1148                                 tax       => $tax });
1149   }
1150
1151   pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1152 }
1153
1154 # get data for saving, printing, ..., that is not changed in the form
1155 #
1156 # Only cvars for now.
1157 sub _get_unalterable_data {
1158   my ($self) = @_;
1159
1160   foreach my $item (@{ $self->order->items }) {
1161     # autovivify all cvars that are not in the form (cvars_by_config can do it).
1162     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1163     foreach my $var (@{ $item->cvars_by_config }) {
1164       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1165     }
1166     $item->parse_custom_variable_values;
1167   }
1168 }
1169
1170 # delete the order
1171 #
1172 # And remove related files in the spool directory
1173 sub _delete {
1174   my ($self) = @_;
1175
1176   my $errors = [];
1177   my $db     = $self->order->db;
1178
1179   $db->with_transaction(
1180     sub {
1181       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1182       $self->order->delete;
1183       my $spool = $::lx_office_conf{paths}->{spool};
1184       unlink map { "$spool/$_" } @spoolfiles if $spool;
1185
1186       1;
1187   }) || push(@{$errors}, $db->error);
1188
1189   return $errors;
1190 }
1191
1192 # save the order
1193 #
1194 # And delete items that are deleted in the form.
1195 sub _save {
1196   my ($self) = @_;
1197
1198   my $errors = [];
1199   my $db     = $self->order->db;
1200
1201   $db->with_transaction(sub {
1202     SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1203     $self->order->save(cascade => 1);
1204   }) || push(@{$errors}, $db->error);
1205
1206   return $errors;
1207 }
1208
1209
1210 sub _pre_render {
1211   my ($self) = @_;
1212
1213   $self->{all_taxzones}             = SL::DB::Manager::TaxZone->get_all_sorted();
1214   $self->{all_departments}          = SL::DB::Manager::Department->get_all_sorted();
1215   $self->{all_employees}            = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1216                                                                                             deleted => 0 ] ],
1217                                                                          sort_by => 'name');
1218   $self->{all_salesmen}             = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1219                                                                                             deleted => 0 ] ],
1220                                                                          sort_by => 'name');
1221   $self->{all_projects}             = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1222                                                                                            active => 1 ] ],
1223                                                                         sort_by => 'projectnumber');
1224   $self->{all_payment_terms}        = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1225                                                                                                       obsolete => 0 ] ]);
1226   $self->{all_delivery_terms}       = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1227   $self->{current_employee_id}      = SL::DB::Manager::Employee->current->id;
1228   $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
1229
1230   my $print_form = Form->new('');
1231   $print_form->{type}      = $self->type;
1232   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
1233   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1234   $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
1235     form => $print_form,
1236     options => {dialog_name_prefix => 'print_options.',
1237                 show_headers       => 1,
1238                 no_queue           => 1,
1239                 no_postscript      => 1,
1240                 no_opendocument    => 1,
1241                 no_html            => 1},
1242   );
1243
1244   foreach my $item (@{$self->order->orderitems}) {
1245     my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1246     $item->active_price_source(   $price_source->price_from_source(   $item->active_price_source   ));
1247     $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1248   }
1249
1250   if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1251     my $webdav = SL::Webdav->new(
1252       type     => $self->type,
1253       number   => $self->order->ordnumber,
1254     );
1255     my @all_objects = $webdav->get_all_objects;
1256     @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1257                                                     type => t8('File'),
1258                                                     link => File::Spec->catfile($_->full_filedescriptor),
1259                                                 } } @all_objects;
1260   }
1261
1262   $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
1263   $self->_setup_edit_action_bar;
1264 }
1265
1266 sub _setup_edit_action_bar {
1267   my ($self, %params) = @_;
1268
1269   my $deletion_allowed = (($self->cv eq 'customer') && $::instance_conf->get_sales_order_show_delete)
1270                       || (($self->cv eq 'vendor')   && $::instance_conf->get_purchase_order_show_delete);
1271
1272   for my $bar ($::request->layout->get('actionbar')) {
1273     $bar->add(
1274       combobox => [
1275         action => [
1276           t8('Save'),
1277           call      => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1278           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1279           accesskey => 'enter',
1280         ],
1281         action => [
1282           t8('Save and Delivery Order'),
1283           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1284           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1285         ],
1286         action => [
1287           t8('Save and Invoice'),
1288           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1289           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1290         ],
1291
1292       ], # end of combobox "Save"
1293
1294       combobox => [
1295         action => [
1296           t8('Export'),
1297         ],
1298         action => [
1299           t8('Print'),
1300           call => [ 'kivi.Order.show_print_options' ],
1301         ],
1302         action => [
1303           t8('E-mail'),
1304           call => [ 'kivi.Order.email' ],
1305         ],
1306         action => [
1307           t8('Download attachments of all parts'),
1308           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1309           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1310           only_if  => $::instance_conf->get_doc_storage,
1311         ],
1312       ], # end of combobox "Export"
1313
1314       action => [
1315         t8('Delete'),
1316         call     => [ 'kivi.Order.delete_order' ],
1317         confirm  => $::locale->text('Do you really want to delete this object?'),
1318         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1319         only_if  => $deletion_allowed,
1320       ],
1321     );
1322   }
1323 }
1324
1325 sub _create_pdf {
1326   my ($order, $pdf_ref, $params) = @_;
1327
1328   my @errors = ();
1329
1330   my $print_form = Form->new('');
1331   $print_form->{type}        = $order->type;
1332   $print_form->{formname}    = $params->{formname} || $order->type;
1333   $print_form->{format}      = $params->{format}   || 'pdf';
1334   $print_form->{media}       = $params->{media}    || 'file';
1335   $print_form->{groupitems}  = $params->{groupitems};
1336   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1337
1338   $order->language($params->{language});
1339   $order->flatten_to_form($print_form, format_amounts => 1);
1340
1341   # search for the template
1342   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1343     name        => $print_form->{formname},
1344     email       => $print_form->{media} eq 'email',
1345     language    => $params->{language},
1346     printer_id  => $print_form->{printer_id},  # todo
1347   );
1348
1349   if (!defined $template_file) {
1350     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);
1351   }
1352
1353   return @errors if scalar @errors;
1354
1355   $print_form->throw_on_error(sub {
1356     eval {
1357       $print_form->prepare_for_printing;
1358
1359       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1360         template  => $template_file,
1361         variables => $print_form,
1362         variable_content_types => {
1363           longdescription => 'html',
1364           partnotes       => 'html',
1365           notes           => 'html',
1366         },
1367       );
1368       1;
1369     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1370   });
1371
1372   return @errors;
1373 }
1374
1375 sub _get_files_for_email_dialog {
1376   my ($self) = @_;
1377
1378   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1379
1380   return %files if !$::instance_conf->get_doc_storage;
1381
1382   if ($self->order->id) {
1383     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
1384     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
1385     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
1386   }
1387
1388   my @parts =
1389     uniq_by { $_->{id} }
1390     map {
1391       +{ id         => $_->part->id,
1392          partnumber => $_->part->partnumber }
1393     } @{$self->order->items_sorted};
1394
1395   foreach my $part (@parts) {
1396     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1397     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1398   }
1399
1400   foreach my $key (keys %files) {
1401     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1402   }
1403
1404   return %files;
1405 }
1406
1407 sub _make_periodic_invoices_config_from_yaml {
1408   my ($yaml_config) = @_;
1409
1410   return if !$yaml_config;
1411   my $attr = YAML::Load($yaml_config);
1412   return if 'HASH' ne ref $attr;
1413   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1414 }
1415
1416
1417 sub _get_periodic_invoices_status {
1418   my ($self, $config) = @_;
1419
1420   return                      if $self->type ne _sales_order_type();
1421   return t8('not configured') if !$config;
1422
1423   my $active = ('HASH' eq ref $config)                           ? $config->{active}
1424              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1425              :                                                     die "Cannot get status of periodic invoices config";
1426
1427   return $active ? t8('active') : t8('inactive');
1428 }
1429
1430 sub _sales_order_type {
1431   'sales_order';
1432 }
1433
1434 sub _purchase_order_type {
1435   'purchase_order';
1436 }
1437
1438 1;
1439
1440 __END__
1441
1442 =encoding utf-8
1443
1444 =head1 NAME
1445
1446 SL::Controller::Order - controller for orders
1447
1448 =head1 SYNOPSIS
1449
1450 This is a new form to enter orders, completely rewritten with the use
1451 of controller and java script techniques.
1452
1453 The aim is to provide the user a better expirience and a faster flow
1454 of work. Also the code should be more readable, more reliable and
1455 better to maintain.
1456
1457 =head2 Key Features
1458
1459 =over 4
1460
1461 =item *
1462
1463 One input row, so that input happens every time at the same place.
1464
1465 =item *
1466
1467 Use of pickers where possible.
1468
1469 =item *
1470
1471 Possibility to enter more than one item at once.
1472
1473 =item *
1474
1475 Save order only on "save" (and "save and delivery order"-workflow). No
1476 hidden save on "print" or "email".
1477
1478 =item *
1479
1480 Item list in a scrollable area, so that the workflow buttons stay at
1481 the bottom.
1482
1483 =item *
1484
1485 Reordering item rows with drag and drop is possible. Sorting item rows is
1486 possible (by partnumber, description, qty, sellprice and discount for now).
1487
1488 =item *
1489
1490 No C<update> is necessary. All entries and calculations are managed
1491 with ajax-calls and the page does only reload on C<save>.
1492
1493 =item *
1494
1495 User can see changes immediately, because of the use of java script
1496 and ajax.
1497
1498 =back
1499
1500 =head1 CODE
1501
1502 =head2 Layout
1503
1504 =over 4
1505
1506 =item * C<SL/Controller/Order.pm>
1507
1508 the controller
1509
1510 =item * C<template/webpages/order/form.html>
1511
1512 main form
1513
1514 =item * C<template/webpages/order/tabs/basic_data.html>
1515
1516 Main tab for basic_data.
1517
1518 This is the only tab here for now. "linked records" and "webdav" tabs are
1519 reused from generic code.
1520
1521 =over 4
1522
1523 =item * C<template/webpages/order/tabs/_item_input.html>
1524
1525 The input line for items
1526
1527 =item * C<template/webpages/order/tabs/_row.html>
1528
1529 One row for already entered items
1530
1531 =item * C<template/webpages/order/tabs/_tax_row.html>
1532
1533 Displaying tax information
1534
1535 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1536
1537 Dialog for entering more than one item at once
1538
1539 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1540
1541 Results for the filter in the multi items dialog
1542
1543 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1544
1545 Dialog for selecting price and discount sources
1546
1547 =back
1548
1549 =item * C<js/kivi.Order.js>
1550
1551 java script functions
1552
1553 =back
1554
1555 =head1 TODO
1556
1557 =over 4
1558
1559 =item * testing
1560
1561 =item * currency
1562
1563 =item * customer/vendor details ('D'-button)
1564
1565 =item * credit limit
1566
1567 =item * more workflows (save as new, quotation, purchase order)
1568
1569 =item * price sources: little symbols showing better price / better discount
1570
1571 =item * select units in input row?
1572
1573 =item * custom shipto address
1574
1575 =item * language / part translations
1576
1577 =item * access rights
1578
1579 =item * display weights
1580
1581 =item * history
1582
1583 =item * mtime check
1584
1585 =item * optional client/user behaviour
1586
1587 (transactions has to be set - department has to be set -
1588  force project if enabled in client config - transport cost reminder)
1589
1590 =back
1591
1592 =head1 KNOWN BUGS AND CAVEATS
1593
1594 =over 4
1595
1596 =item *
1597
1598 Customer discount is not displayed as a valid discount in price source popup
1599 (this might be a bug in price sources)
1600
1601 (I cannot reproduce this (Bernd))
1602
1603 =item *
1604
1605 No indication that <shift>-up/down expands/collapses second row.
1606
1607 =item *
1608
1609 Inline creation of parts is not currently supported
1610
1611 =item *
1612
1613 Table header is not sticky in the scrolling area.
1614
1615 =item *
1616
1617 Sorting does not include C<position>, neither does reordering.
1618
1619 This behavior was implemented intentionally. But we can discuss, which behavior
1620 should be implemented.
1621
1622 =item *
1623
1624 C<show_multi_items_dialog> does not use the currently inserted string for
1625 filtering.
1626
1627 =item *
1628
1629 The language selected in print or email dialog is not saved when the order is saved.
1630
1631 =back
1632
1633 =head1 To discuss / Nice to have
1634
1635 =over 4
1636
1637 =item *
1638
1639 How to expand/collapse second row. Now it can be done clicking the icon or
1640 <shift>-up/down.
1641
1642 =item *
1643
1644 Possibility to change longdescription in input row?
1645
1646 =item *
1647
1648 Possibility to select PriceSources in input row?
1649
1650 =item *
1651
1652 This controller uses a (changed) copy of the template for the PriceSource
1653 dialog. Maybe there could be used one code source.
1654
1655 =item *
1656
1657 Rounding-differences between this controller (PriceTaxCalculator) and the old
1658 form. This is not only a problem here, but also in all parts using the PTC.
1659 There exists a ticket and a patch. This patch should be testet.
1660
1661 =item *
1662
1663 An indicator, if the actual inputs are saved (like in an
1664 editor or on text processing application).
1665
1666 =item *
1667
1668 A warning when leaving the page without saveing unchanged inputs.
1669
1670 =item *
1671
1672 Workflows for delivery order and invoice are in the menu "Save", because the
1673 order is saved before opening the new document form. Nevertheless perhaps these
1674 workflow buttons should be put under "Workflows".
1675
1676
1677 =back
1678
1679 =head1 AUTHOR
1680
1681 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1682
1683 =cut