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