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