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