]> wagnertech.de Git - kivitendo-erp.git/blob - SL/Controller/Order.pm
Auftrags-Controller: Speichern nicht accesskey "enter"
[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         ],
1311         action => [
1312           t8('Save and Delivery Order'),
1313           call      => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1314           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1315           only_if   => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
1316         ],
1317         action => [
1318           t8('Save and Invoice'),
1319           call      => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1320           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1321         ],
1322
1323       ], # end of combobox "Save"
1324
1325       combobox => [
1326         action => [
1327           t8('Export'),
1328         ],
1329         action => [
1330           t8('Print'),
1331           call => [ 'kivi.Order.show_print_options' ],
1332         ],
1333         action => [
1334           t8('E-mail'),
1335           call => [ 'kivi.Order.email' ],
1336         ],
1337         action => [
1338           t8('Download attachments of all parts'),
1339           call     => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1340           disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1341           only_if  => $::instance_conf->get_doc_storage,
1342         ],
1343       ], # end of combobox "Export"
1344
1345       action => [
1346         t8('Delete'),
1347         call     => [ 'kivi.Order.delete_order' ],
1348         confirm  => $::locale->text('Do you really want to delete this object?'),
1349         disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1350         only_if  => $deletion_allowed,
1351       ],
1352     );
1353   }
1354 }
1355
1356 sub _create_pdf {
1357   my ($order, $pdf_ref, $params) = @_;
1358
1359   my @errors = ();
1360
1361   my $print_form = Form->new('');
1362   $print_form->{type}        = $order->type;
1363   $print_form->{formname}    = $params->{formname} || $order->type;
1364   $print_form->{format}      = $params->{format}   || 'pdf';
1365   $print_form->{media}       = $params->{media}    || 'file';
1366   $print_form->{groupitems}  = $params->{groupitems};
1367   $print_form->{media}       = 'file'                             if $print_form->{media} eq 'screen';
1368
1369   $order->language($params->{language});
1370   $order->flatten_to_form($print_form, format_amounts => 1);
1371
1372   # search for the template
1373   my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1374     name        => $print_form->{formname},
1375     email       => $print_form->{media} eq 'email',
1376     language    => $params->{language},
1377     printer_id  => $print_form->{printer_id},  # todo
1378   );
1379
1380   if (!defined $template_file) {
1381     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);
1382   }
1383
1384   return @errors if scalar @errors;
1385
1386   $print_form->throw_on_error(sub {
1387     eval {
1388       $print_form->prepare_for_printing;
1389
1390       $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1391         template  => $template_file,
1392         variables => $print_form,
1393         variable_content_types => {
1394           longdescription => 'html',
1395           partnotes       => 'html',
1396           notes           => 'html',
1397         },
1398       );
1399       1;
1400     } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1401   });
1402
1403   return @errors;
1404 }
1405
1406 sub _get_files_for_email_dialog {
1407   my ($self) = @_;
1408
1409   my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1410
1411   return %files if !$::instance_conf->get_doc_storage;
1412
1413   if ($self->order->id) {
1414     $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id,              object_type => $self->order->type, file_type => 'document') ];
1415     $files{files}    = [ SL::File->get_all(         object_id => $self->order->id,              object_type => $self->order->type, file_type => 'attachment') ];
1416     $files{vc_files} = [ SL::File->get_all(         object_id => $self->order->{$self->cv}->id, object_type => $self->cv,          file_type => 'attachment') ];
1417   }
1418
1419   my @parts =
1420     uniq_by { $_->{id} }
1421     map {
1422       +{ id         => $_->part->id,
1423          partnumber => $_->part->partnumber }
1424     } @{$self->order->items_sorted};
1425
1426   foreach my $part (@parts) {
1427     my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1428     push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1429   }
1430
1431   foreach my $key (keys %files) {
1432     $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1433   }
1434
1435   return %files;
1436 }
1437
1438 sub _make_periodic_invoices_config_from_yaml {
1439   my ($yaml_config) = @_;
1440
1441   return if !$yaml_config;
1442   my $attr = YAML::Load($yaml_config);
1443   return if 'HASH' ne ref $attr;
1444   return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1445 }
1446
1447
1448 sub _get_periodic_invoices_status {
1449   my ($self, $config) = @_;
1450
1451   return                      if $self->type ne _sales_order_type();
1452   return t8('not configured') if !$config;
1453
1454   my $active = ('HASH' eq ref $config)                           ? $config->{active}
1455              : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1456              :                                                     die "Cannot get status of periodic invoices config";
1457
1458   return $active ? t8('active') : t8('inactive');
1459 }
1460
1461 sub _sales_order_type {
1462   'sales_order';
1463 }
1464
1465 sub _purchase_order_type {
1466   'purchase_order';
1467 }
1468
1469 sub _sales_quotation_type {
1470   'sales_quotation';
1471 }
1472
1473 sub _request_quotation_type {
1474   'request_quotation';
1475 }
1476
1477 1;
1478
1479 __END__
1480
1481 =encoding utf-8
1482
1483 =head1 NAME
1484
1485 SL::Controller::Order - controller for orders
1486
1487 =head1 SYNOPSIS
1488
1489 This is a new form to enter orders, completely rewritten with the use
1490 of controller and java script techniques.
1491
1492 The aim is to provide the user a better expirience and a faster flow
1493 of work. Also the code should be more readable, more reliable and
1494 better to maintain.
1495
1496 =head2 Key Features
1497
1498 =over 4
1499
1500 =item *
1501
1502 One input row, so that input happens every time at the same place.
1503
1504 =item *
1505
1506 Use of pickers where possible.
1507
1508 =item *
1509
1510 Possibility to enter more than one item at once.
1511
1512 =item *
1513
1514 Save order only on "save" (and "save and delivery order"-workflow). No
1515 hidden save on "print" or "email".
1516
1517 =item *
1518
1519 Item list in a scrollable area, so that the workflow buttons stay at
1520 the bottom.
1521
1522 =item *
1523
1524 Reordering item rows with drag and drop is possible. Sorting item rows is
1525 possible (by partnumber, description, qty, sellprice and discount for now).
1526
1527 =item *
1528
1529 No C<update> is necessary. All entries and calculations are managed
1530 with ajax-calls and the page does only reload on C<save>.
1531
1532 =item *
1533
1534 User can see changes immediately, because of the use of java script
1535 and ajax.
1536
1537 =back
1538
1539 =head1 CODE
1540
1541 =head2 Layout
1542
1543 =over 4
1544
1545 =item * C<SL/Controller/Order.pm>
1546
1547 the controller
1548
1549 =item * C<template/webpages/order/form.html>
1550
1551 main form
1552
1553 =item * C<template/webpages/order/tabs/basic_data.html>
1554
1555 Main tab for basic_data.
1556
1557 This is the only tab here for now. "linked records" and "webdav" tabs are
1558 reused from generic code.
1559
1560 =over 4
1561
1562 =item * C<template/webpages/order/tabs/_item_input.html>
1563
1564 The input line for items
1565
1566 =item * C<template/webpages/order/tabs/_row.html>
1567
1568 One row for already entered items
1569
1570 =item * C<template/webpages/order/tabs/_tax_row.html>
1571
1572 Displaying tax information
1573
1574 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1575
1576 Dialog for entering more than one item at once
1577
1578 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1579
1580 Results for the filter in the multi items dialog
1581
1582 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1583
1584 Dialog for selecting price and discount sources
1585
1586 =back
1587
1588 =item * C<js/kivi.Order.js>
1589
1590 java script functions
1591
1592 =back
1593
1594 =head1 TODO
1595
1596 =over 4
1597
1598 =item * testing
1599
1600 =item * currency
1601
1602 =item * customer/vendor details ('D'-button)
1603
1604 =item * credit limit
1605
1606 =item * more workflows (save as new, quotation, purchase order)
1607
1608 =item * price sources: little symbols showing better price / better discount
1609
1610 =item * select units in input row?
1611
1612 =item * custom shipto address
1613
1614 =item * language / part translations
1615
1616 =item * access rights
1617
1618 =item * display weights
1619
1620 =item * history
1621
1622 =item * mtime check
1623
1624 =item * optional client/user behaviour
1625
1626 (transactions has to be set - department has to be set -
1627  force project if enabled in client config - transport cost reminder)
1628
1629 =back
1630
1631 =head1 KNOWN BUGS AND CAVEATS
1632
1633 =over 4
1634
1635 =item *
1636
1637 Customer discount is not displayed as a valid discount in price source popup
1638 (this might be a bug in price sources)
1639
1640 (I cannot reproduce this (Bernd))
1641
1642 =item *
1643
1644 No indication that <shift>-up/down expands/collapses second row.
1645
1646 =item *
1647
1648 Inline creation of parts is not currently supported
1649
1650 =item *
1651
1652 Table header is not sticky in the scrolling area.
1653
1654 =item *
1655
1656 Sorting does not include C<position>, neither does reordering.
1657
1658 This behavior was implemented intentionally. But we can discuss, which behavior
1659 should be implemented.
1660
1661 =item *
1662
1663 C<show_multi_items_dialog> does not use the currently inserted string for
1664 filtering.
1665
1666 =item *
1667
1668 The language selected in print or email dialog is not saved when the order is saved.
1669
1670 =back
1671
1672 =head1 To discuss / Nice to have
1673
1674 =over 4
1675
1676 =item *
1677
1678 How to expand/collapse second row. Now it can be done clicking the icon or
1679 <shift>-up/down.
1680
1681 =item *
1682
1683 Possibility to change longdescription in input row?
1684
1685 =item *
1686
1687 Possibility to select PriceSources in input row?
1688
1689 =item *
1690
1691 This controller uses a (changed) copy of the template for the PriceSource
1692 dialog. Maybe there could be used one code source.
1693
1694 =item *
1695
1696 Rounding-differences between this controller (PriceTaxCalculator) and the old
1697 form. This is not only a problem here, but also in all parts using the PTC.
1698 There exists a ticket and a patch. This patch should be testet.
1699
1700 =item *
1701
1702 An indicator, if the actual inputs are saved (like in an
1703 editor or on text processing application).
1704
1705 =item *
1706
1707 A warning when leaving the page without saveing unchanged inputs.
1708
1709 =item *
1710
1711 Workflows for delivery order and invoice are in the menu "Save", because the
1712 order is saved before opening the new document form. Nevertheless perhaps these
1713 workflow buttons should be put under "Workflows".
1714
1715
1716 =back
1717
1718 =head1 AUTHOR
1719
1720 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
1721
1722 =cut