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