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