1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
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;
21 use SL::Helper::CreatePDF qw(:all);
22 use SL::Helper::PrintOptions;
24 use SL::Controller::Helper::GetModels;
26 use List::Util qw(first);
27 use List::UtilsBy qw(sort_by uniq_by);
28 use List::MoreUtils qw(any none pairwise first_index);
29 use English qw(-no_match_vars);
33 use Rose::Object::MakeMethods::Generic
35 scalar => [ qw(item_ids_to_delete) ],
36 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
41 __PACKAGE__->run_before('_check_auth');
43 __PACKAGE__->run_before('_recalc',
44 only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
46 __PACKAGE__->run_before('_get_unalterable_data',
47 only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
57 $self->order->transdate(DateTime->now_local());
58 $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
63 title => $self->type eq _sales_order_type() ? $::locale->text('Add Sales Order')
64 : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
65 : $self->type eq _sales_quotation_type() ? $::locale->text('Add Quotation')
66 : $self->type eq _request_quotation_type() ? $::locale->text('Add Request for Quotation')
68 %{$self->{template_args}}
72 # edit an existing order
81 title => $self->type eq _sales_order_type() ? $::locale->text('Edit Sales Order')
82 : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
83 : $self->type eq _sales_quotation_type() ? $::locale->text('Edit Quotation')
84 : $self->type eq _request_quotation_type() ? $::locale->text('Edit Request for Quotation')
86 %{$self->{template_args}}
94 my $errors = $self->_delete();
96 if (scalar @{ $errors }) {
97 $self->js->flash('error', $_) foreach @{ $errors };
98 return $self->js->render();
101 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been deleted')
102 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been deleted')
103 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been deleted')
104 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been deleted')
106 flash_later('info', $text);
108 my @redirect_params = (
113 $self->redirect_to(@redirect_params);
120 my $errors = $self->_save();
122 if (scalar @{ $errors }) {
123 $self->js->flash('error', $_) foreach @{ $errors };
124 return $self->js->render();
127 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
128 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
129 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
130 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
132 flash_later('info', $text);
134 my @redirect_params = (
137 id => $self->order->id,
140 $self->redirect_to(@redirect_params);
145 # This is called if "print" is pressed in the print dialog.
146 # If PDF creation was requested and succeeded, the pdf is stored in a session
147 # file and the filename is stored as session value with an unique key. A
148 # javascript function with this key is then called. This function calls the
149 # download action below (action_download_pdf), which offers the file for
154 my $format = $::form->{print_options}->{format};
155 my $media = $::form->{print_options}->{media};
156 my $formname = $::form->{print_options}->{formname};
157 my $copies = $::form->{print_options}->{copies};
158 my $groupitems = $::form->{print_options}->{groupitems};
161 if (none { $format eq $_ } qw(pdf)) {
162 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
165 # only screen or printer by now
166 if (none { $media eq $_ } qw(screen printer)) {
167 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
171 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
173 # create a form for generate_attachment_filename
174 my $form = Form->new;
175 $form->{ordnumber} = $self->order->ordnumber;
176 $form->{type} = $self->type;
177 $form->{format} = $format;
178 $form->{formname} = $formname;
179 $form->{language} = '_' . $language->template_code if $language;
180 my $pdf_filename = $form->generate_attachment_filename();
183 my @errors = _create_pdf($self->order, \$pdf, { format => $format,
184 formname => $formname,
185 language => $language,
186 groupitems => $groupitems });
187 if (scalar @errors) {
188 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
191 if ($media eq 'screen') {
193 my $sfile = SL::SessionFile::Random->new(mode => "w");
194 $sfile->fh->print($pdf);
197 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
198 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
201 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
202 ->flash('info', t8('The PDF has been created'));
204 } elsif ($media eq 'printer') {
206 my $printer_id = $::form->{print_options}->{printer_id};
207 SL::DB::Printer->new(id => $printer_id)->load->print_document(
212 $self->js->flash('info', t8('The PDF has been printed'));
215 # copy file to webdav folder
216 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
217 my $webdav = SL::Webdav->new(
219 number => $self->order->ordnumber,
221 my $webdav_file = SL::Webdav::File->new(
223 filename => $pdf_filename,
226 $webdav_file->store(data => \$pdf);
229 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
232 if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
234 SL::File->save(object_id => $self->order->id,
235 object_type => $self->type,
236 mime_type => 'application/pdf',
238 file_type => 'document',
239 file_name => $pdf_filename,
240 file_contents => $pdf);
243 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
249 # offer pdf for download
251 # It needs to get the key for the session value to get the pdf file.
252 sub action_download_pdf {
255 my $key = $::form->{key};
256 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
257 return $self->send_file(
259 type => 'application/pdf',
260 name => $::form->{pdf_filename},
264 # open the email dialog
265 sub action_show_email_dialog {
268 my $cv_method = $self->cv;
270 if (!$self->order->$cv_method) {
271 return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
276 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
277 $email_form->{to} ||= $self->order->$cv_method->email;
278 $email_form->{cc} = $self->order->$cv_method->cc;
279 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
280 # Todo: get addresses from shipto, if any
282 my $form = Form->new;
283 $form->{ordnumber} = $self->order->ordnumber;
284 $form->{formname} = $self->type;
285 $form->{type} = $self->type;
286 $form->{language} = 'de';
287 $form->{format} = 'pdf';
289 $email_form->{subject} = $form->generate_email_subject();
290 $email_form->{attachment_filename} = $form->generate_attachment_filename();
291 $email_form->{message} = $form->generate_email_body();
292 $email_form->{js_send_function} = 'kivi.Order.send_email()';
294 my %files = $self->_get_files_for_email_dialog();
295 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
296 email_form => $email_form,
297 show_bcc => $::auth->assert('email_bcc', 'may fail'),
299 is_customer => $self->cv eq 'customer',
303 ->run('kivi.Order.show_email_dialog', $dialog_html)
310 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
311 sub action_send_email {
314 my $email_form = delete $::form->{email_form};
315 my %field_names = (to => 'email');
317 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
319 # for Form::cleanup which may be called in Form::send_email
320 $::form->{cwd} = getcwd();
321 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
323 $::form->{media} = 'email';
325 if (($::form->{attachment_policy} // '') eq 'normal') {
327 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
330 my @errors = _create_pdf($self->order, \$pdf, {media => $::form->{media},
331 format => $::form->{print_options}->{format},
332 formname => $::form->{print_options}->{formname},
333 language => $language,
334 groupitems => $::form->{print_options}->{groupitems}});
335 if (scalar @errors) {
336 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
339 my $sfile = SL::SessionFile::Random->new(mode => "w");
340 $sfile->fh->print($pdf);
343 $::form->{tmpfile} = $sfile->file_name;
344 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
347 $::form->send_email(\%::myconfig, 'pdf');
350 my $intnotes = $self->order->intnotes;
351 $intnotes .= "\n\n" if $self->order->intnotes;
352 $intnotes .= t8('[email]') . "\n";
353 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
354 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
355 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
356 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
357 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
358 $intnotes .= t8('Message') . ": " . $::form->{message};
361 ->val('#order_intnotes', $intnotes)
362 ->run('kivi.Order.close_email_dialog')
363 ->flash('info', t8('The email has been sent.'))
367 # open the periodic invoices config dialog
369 # If there are values in the form (i.e. dialog was opened before),
370 # then use this values. Create new ones, else.
371 sub action_show_periodic_invoices_config_dialog {
374 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
375 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
376 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
377 order_value_periodicity => 'p', # = same as periodicity
378 start_date_as_date => $::form->{transdate} || $::form->current_date,
379 extend_automatically_by => 12,
381 email_subject => GenericTranslations->get(
382 language_id => $::form->{language_id},
383 translation_type =>"preset_text_periodic_invoices_email_subject"),
384 email_body => GenericTranslations->get(
385 language_id => $::form->{language_id},
386 translation_type =>"preset_text_periodic_invoices_email_body"),
388 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
389 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
391 $::form->get_lists(printers => "ALL_PRINTERS",
392 charts => { key => 'ALL_CHARTS',
393 transdate => 'current_date' });
395 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
397 if ($::form->{customer_id}) {
398 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
401 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
403 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
404 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
409 # assign the values of the periodic invoices config dialog
410 # as yaml in the hidden tag and set the status.
411 sub action_assign_periodic_invoices_config {
414 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
416 my $config = { active => $::form->{active} ? 1 : 0,
417 terminated => $::form->{terminated} ? 1 : 0,
418 direct_debit => $::form->{direct_debit} ? 1 : 0,
419 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
420 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
421 start_date_as_date => $::form->{start_date_as_date},
422 end_date_as_date => $::form->{end_date_as_date},
423 first_billing_date_as_date => $::form->{first_billing_date_as_date},
424 print => $::form->{print} ? 1 : 0,
425 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
426 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
427 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
428 ar_chart_id => $::form->{ar_chart_id} * 1,
429 send_email => $::form->{send_email} ? 1 : 0,
430 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
431 email_recipient_address => $::form->{email_recipient_address},
432 email_sender => $::form->{email_sender},
433 email_subject => $::form->{email_subject},
434 email_body => $::form->{email_body},
437 my $periodic_invoices_config = YAML::Dump($config);
439 my $status = $self->_get_periodic_invoices_status($config);
442 ->remove('#order_periodic_invoices_config')
443 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
444 ->run('kivi.Order.close_periodic_invoices_config_dialog')
445 ->html('#periodic_invoices_status', $status)
446 ->flash('info', t8('The periodic invoices config has been assigned.'))
450 sub action_get_has_active_periodic_invoices {
453 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
454 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
456 my $has_active_periodic_invoices =
457 $self->type eq _sales_order_type()
460 && (!$config->end_date || ($config->end_date > DateTime->today_local))
461 && $config->get_previous_billed_period_start_date;
463 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
466 # save the order and redirect to the frontend subroutine for a new
468 sub action_save_and_delivery_order {
471 my $errors = $self->_save();
473 if (scalar @{ $errors }) {
474 $self->js->flash('error', $_) foreach @{ $errors };
475 return $self->js->render();
478 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
479 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
480 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
481 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
483 flash_later('info', $text);
485 my @redirect_params = (
486 controller => 'oe.pl',
487 action => 'oe_delivery_order_from_order',
488 id => $self->order->id,
491 $self->redirect_to(@redirect_params);
494 # save the order and redirect to the frontend subroutine for a new
496 sub action_save_and_invoice {
499 my $errors = $self->_save();
501 if (scalar @{ $errors }) {
502 $self->js->flash('error', $_) foreach @{ $errors };
503 return $self->js->render();
506 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
507 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
508 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
509 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
511 flash_later('info', $text);
513 my @redirect_params = (
514 controller => 'oe.pl',
515 action => 'oe_invoice_from_order',
516 id => $self->order->id,
519 $self->redirect_to(@redirect_params);
522 # set form elements in respect to a changed customer or vendor
524 # This action is called on an change of the customer/vendor picker.
525 sub action_customer_vendor_changed {
528 my $cv_method = $self->cv;
530 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
531 $self->js->show('#cp_row');
533 $self->js->hide('#cp_row');
536 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
537 $self->js->show('#shipto_row');
539 $self->js->hide('#shipto_row');
542 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
544 if ($self->order->is_sales) {
545 $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
546 ? $self->order->$cv_method->taxincluded_checked
547 : $::myconfig{taxincluded_checked});
548 $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
551 $self->order->payment_id($self->order->$cv_method->payment_id);
552 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
557 ->replaceWith('#order_cp_id', $self->build_contact_select)
558 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
559 ->val( '#order_taxzone_id', $self->order->taxzone_id)
560 ->val( '#order_taxincluded', $self->order->taxincluded)
561 ->val( '#order_payment_id', $self->order->payment_id)
562 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
563 ->val( '#order_intnotes', $self->order->$cv_method->notes)
564 ->focus( '#order_' . $self->cv . '_id');
566 $self->_js_redisplay_amounts_and_taxes;
570 # called if a unit in an existing item row is changed
571 sub action_unit_changed {
574 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
575 my $item = $self->order->items_sorted->[$idx];
577 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
578 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
583 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
584 $self->_js_redisplay_line_values;
585 $self->_js_redisplay_amounts_and_taxes;
589 # add an item row for a new item entered in the input row
590 sub action_add_item {
593 my $form_attr = $::form->{add_item};
595 return unless $form_attr->{parts_id};
597 my $item = _new_item($self->order, $form_attr);
599 $self->order->add_items($item);
603 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
604 my $row_as_html = $self->p->render('order/tabs/_row',
608 ALL_PRICE_FACTORS => $self->all_price_factors
612 ->append('#row_table_id', $row_as_html);
614 if ( $item->part->is_assortment ) {
615 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
616 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
617 my $attr = { parts_id => $assortment_item->parts_id,
618 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
619 unit => $assortment_item->unit,
620 description => $assortment_item->part->description,
622 my $item = _new_item($self->order, $attr);
624 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
625 $item->discount(1) unless $assortment_item->charge;
627 $self->order->add_items( $item );
629 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
630 my $row_as_html = $self->p->render('order/tabs/_row',
634 ALL_PRICE_FACTORS => $self->all_price_factors
637 ->append('#row_table_id', $row_as_html);
642 ->val('.add_item_input', '')
643 ->run('kivi.Order.init_row_handlers')
644 ->run('kivi.Order.row_table_scroll_down')
645 ->run('kivi.Order.renumber_positions')
646 ->focus('#add_item_parts_id_name');
648 $self->_js_redisplay_amounts_and_taxes;
652 # open the dialog for entering multiple items at once
653 sub action_show_multi_items_dialog {
654 require SL::DB::PartsGroup;
655 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
656 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
659 # update the filter results in the multi item dialog
660 sub action_multi_items_update_result {
663 $::form->{multi_items}->{filter}->{obsolete} = 0;
665 my $count = $_[0]->multi_items_models->count;
668 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
669 $_[0]->render($text, { layout => 0 });
670 } elsif ($count > $max_count) {
671 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
672 $_[0]->render($text, { layout => 0 });
674 my $multi_items = $_[0]->multi_items_models->get;
675 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
676 multi_items => $multi_items);
680 # add item rows for multiple items at once
681 sub action_add_multi_items {
684 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
685 return $self->js->render() unless scalar @form_attr;
688 foreach my $attr (@form_attr) {
689 my $item = _new_item($self->order, $attr);
691 if ( $item->part->is_assortment ) {
692 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
693 my $attr = { parts_id => $assortment_item->parts_id,
694 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
695 unit => $assortment_item->unit,
696 description => $assortment_item->part->description,
698 my $item = _new_item($self->order, $attr);
700 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
701 $item->discount(1) unless $assortment_item->charge;
706 $self->order->add_items(@items);
710 foreach my $item (@items) {
711 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
712 my $row_as_html = $self->p->render('order/tabs/_row',
716 ALL_PRICE_FACTORS => $self->all_price_factors
719 $self->js->append('#row_table_id', $row_as_html);
723 ->run('kivi.Order.close_multi_items_dialog')
724 ->run('kivi.Order.init_row_handlers')
725 ->run('kivi.Order.row_table_scroll_down')
726 ->run('kivi.Order.renumber_positions')
727 ->focus('#add_item_parts_id_name');
729 $self->_js_redisplay_amounts_and_taxes;
733 # recalculate all linetotals, amounts and taxes and redisplay them
734 sub action_recalc_amounts_and_taxes {
739 $self->_js_redisplay_line_values;
740 $self->_js_redisplay_amounts_and_taxes;
744 # redisplay item rows if they are sorted by an attribute
745 sub action_reorder_items {
749 partnumber => sub { $_[0]->part->partnumber },
750 description => sub { $_[0]->description },
751 qty => sub { $_[0]->qty },
752 sellprice => sub { $_[0]->sellprice },
753 discount => sub { $_[0]->discount },
756 my $method = $sort_keys{$::form->{order_by}};
757 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
758 if ($::form->{sort_dir}) {
759 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
761 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
764 ->run('kivi.Order.redisplay_items', \@to_sort)
768 # show the popup to choose a price/discount source
769 sub action_price_popup {
772 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
773 my $item = $self->order->items_sorted->[$idx];
775 $self->render_price_dialog($item);
778 # get the longdescription for an item if the dialog to enter/change the
779 # longdescription was opened and the longdescription is empty
781 # If this item is new, get the longdescription from Part.
782 # Otherwise get it from OrderItem.
783 sub action_get_item_longdescription {
786 if ($::form->{item_id}) {
787 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
788 } elsif ($::form->{parts_id}) {
789 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
791 $_[0]->render(\ $longdescription, { type => 'text' });
794 # load the second row for one or more items
796 # This action gets the html code for all items second rows by rendering a template for
797 # the second row and sets the html code via client js.
798 sub action_load_second_rows {
801 $self->_recalc() if $self->order->is_sales; # for margin calculation
803 foreach my $item_id (@{ $::form->{item_ids} }) {
804 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
805 my $item = $self->order->items_sorted->[$idx];
807 $self->_js_load_second_row($item, $item_id, 0);
810 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
815 sub _js_load_second_row {
816 my ($self, $item, $item_id, $do_parse) = @_;
819 # Parse values from form (they are formated while rendering (template)).
820 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
821 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
822 foreach my $var (@{ $item->cvars_by_config }) {
823 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
825 $item->parse_custom_variable_values;
828 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
831 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
832 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
835 sub _js_redisplay_line_values {
838 my $is_sales = $self->order->is_sales;
840 # sales orders with margins
845 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
846 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
847 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
848 ]} @{ $self->order->items_sorted };
852 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
853 ]} @{ $self->order->items_sorted };
857 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
860 sub _js_redisplay_amounts_and_taxes {
863 if (scalar @{ $self->{taxes} }) {
864 $self->js->show('#taxincluded_row_id');
866 $self->js->hide('#taxincluded_row_id');
869 if ($self->order->taxincluded) {
870 $self->js->hide('#subtotal_row_id');
872 $self->js->show('#subtotal_row_id');
876 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
877 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
879 ->insertBefore($self->build_tax_rows, '#amount_row_id');
886 sub init_valid_types {
887 [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
893 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
894 die "Not a valid type for order";
897 $self->type($::form->{type});
903 my $cv = (any { $self->type eq $_ } (_sales_order_type(), _sales_quotation_type())) ? 'customer'
904 : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
905 : die "Not a valid type for order";
918 # model used to filter/display the parts in the multi-items dialog
919 sub init_multi_items_models {
920 SL::Controller::Helper::GetModels->new(
923 with_objects => [ qw(unit_obj) ],
924 disable_plugin => 'paginated',
925 source => $::form->{multi_items},
931 partnumber => t8('Partnumber'),
932 description => t8('Description')}
936 sub init_all_price_factors {
937 SL::DB::Manager::PriceFactor->get_all;
943 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
945 my $right = $right_for->{ $self->type };
946 $right ||= 'DOES_NOT_EXIST';
948 $::auth->assert($right);
951 # build the selection box for contacts
953 # Needed, if customer/vendor changed.
954 sub build_contact_select {
957 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
958 value_key => 'cp_id',
959 title_key => 'full_name_dep',
960 default => $self->order->cp_id,
962 style => 'width: 300px',
966 # build the selection box for shiptos
968 # Needed, if customer/vendor changed.
969 sub build_shipto_select {
972 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
973 value_key => 'shipto_id',
974 title_key => 'displayable_id',
975 default => $self->order->shipto_id,
977 style => 'width: 300px',
981 # build the rows for displaying taxes
983 # Called if amounts where recalculated and redisplayed.
988 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
989 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
991 return $rows_as_html;
995 sub render_price_dialog {
996 my ($self, $record_item) = @_;
998 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1002 'kivi.io.price_chooser_dialog',
1003 t8('Available Prices'),
1004 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1009 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1010 # $self->js->show('#dialog_flash_error');
1019 return if !$::form->{id};
1021 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
1024 # load or create a new order object
1026 # And assign changes from the form to this object.
1027 # If the order is loaded from db, check if items are deleted in the form,
1028 # remove them form the object and collect them for removing from db on saving.
1029 # Then create/update items from form (via _make_item) and add them.
1033 # add_items adds items to an order with no items for saving, but they cannot
1034 # be retrieved via items until the order is saved. Adding empty items to new
1035 # order here solves this problem.
1037 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1038 $order ||= SL::DB::Order->new(orderitems => [],
1039 quotation => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
1041 my $form_orderitems = delete $::form->{order}->{orderitems};
1042 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1044 $order->assign_attributes(%{$::form->{order}});
1046 my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1047 $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1049 # remove deleted items
1050 $self->item_ids_to_delete([]);
1051 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1052 my $item = $order->orderitems->[$idx];
1053 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1054 splice @{$order->orderitems}, $idx, 1;
1055 push @{$self->item_ids_to_delete}, $item->id;
1061 foreach my $form_attr (@{$form_orderitems}) {
1062 my $item = _make_item($order, $form_attr);
1063 $item->position($pos);
1067 $order->add_items(grep {!$_->id} @items);
1072 # create or update items from form
1074 # Make item objects from form values. For items already existing read from db.
1075 # Create a new item else. And assign attributes.
1077 my ($record, $attr) = @_;
1080 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1082 my $is_new = !$item;
1084 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1085 # they cannot be retrieved via custom_variables until the order/orderitem is
1086 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1087 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1089 $item->assign_attributes(%$attr);
1090 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1091 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1092 $item->lastcost($item->part->lastcost) if $is_new && !defined $attr->{lastcost_as_number};
1099 # This is used to add one item
1101 my ($record, $attr) = @_;
1103 my $item = SL::DB::OrderItem->new;
1104 $item->assign_attributes(%$attr);
1106 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1107 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1109 $item->unit($part->unit) if !$item->unit;
1112 if ( $part->is_assortment ) {
1113 # add assortment items with price 0, as the components carry the price
1114 $price_src = $price_source->price_from_source("");
1115 $price_src->price(0);
1116 } elsif ($item->sellprice) {
1117 $price_src = $price_source->price_from_source("");
1118 $price_src->price($item->sellprice);
1120 $price_src = $price_source->best_price
1121 ? $price_source->best_price
1122 : $price_source->price_from_source("");
1123 $price_src->price(0) if !$price_source->best_price;
1127 if ($item->discount) {
1128 $discount_src = $price_source->discount_from_source("");
1129 $discount_src->discount($item->discount);
1131 $discount_src = $price_source->best_discount
1132 ? $price_source->best_discount
1133 : $price_source->discount_from_source("");
1134 $discount_src->discount(0) if !$price_source->best_discount;
1138 $new_attr{part} = $part;
1139 $new_attr{description} = $part->description if ! $item->description;
1140 $new_attr{qty} = 1.0 if ! $item->qty;
1141 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1142 $new_attr{sellprice} = $price_src->price;
1143 $new_attr{discount} = $discount_src->discount;
1144 $new_attr{active_price_source} = $price_src;
1145 $new_attr{active_discount_source} = $discount_src;
1146 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1147 $new_attr{project_id} = $record->globalproject_id;
1148 $new_attr{lastcost} = $part->lastcost;
1150 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1151 # they cannot be retrieved via custom_variables until the order/orderitem is
1152 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1153 $new_attr{custom_variables} = [];
1155 $item->assign_attributes(%new_attr);
1160 # recalculate prices and taxes
1162 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1166 # bb: todo: currency later
1167 $self->order->currency_id($::instance_conf->get_currency_id());
1169 my %pat = $self->order->calculate_prices_and_taxes();
1170 $self->{taxes} = [];
1171 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1172 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1174 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1175 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1176 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1180 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1183 # get data for saving, printing, ..., that is not changed in the form
1185 # Only cvars for now.
1186 sub _get_unalterable_data {
1189 foreach my $item (@{ $self->order->items }) {
1190 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1191 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1192 foreach my $var (@{ $item->cvars_by_config }) {
1193 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1195 $item->parse_custom_variable_values;
1201 # And remove related files in the spool directory
1206 my $db = $self->order->db;
1208 $db->with_transaction(
1210 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1211 $self->order->delete;
1212 my $spool = $::lx_office_conf{paths}->{spool};
1213 unlink map { "$spool/$_" } @spoolfiles if $spool;
1216 }) || push(@{$errors}, $db->error);
1223 # And delete items that are deleted in the form.
1228 my $db = $self->order->db;
1230 $db->with_transaction(sub {
1231 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1232 $self->order->save(cascade => 1);
1233 }) || push(@{$errors}, $db->error);
1242 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1243 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1244 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1247 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1250 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1252 sort_by => 'projectnumber');
1253 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1255 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1256 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1257 $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
1258 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1260 my $print_form = Form->new('');
1261 $print_form->{type} = $self->type;
1262 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1263 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1264 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1265 form => $print_form,
1266 options => {dialog_name_prefix => 'print_options.',
1270 no_opendocument => 1,
1274 foreach my $item (@{$self->order->orderitems}) {
1275 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1276 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1277 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1280 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1281 my $webdav = SL::Webdav->new(
1282 type => $self->type,
1283 number => $self->order->ordnumber,
1285 my @all_objects = $webdav->get_all_objects;
1286 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1288 link => File::Spec->catfile($_->full_filedescriptor),
1292 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
1293 $self->_setup_edit_action_bar;
1296 sub _setup_edit_action_bar {
1297 my ($self, %params) = @_;
1299 my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
1300 || (($self->type eq _sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1301 || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1303 for my $bar ($::request->layout->get('actionbar')) {
1308 call => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1309 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1310 accesskey => 'enter',
1313 t8('Save and Delivery Order'),
1314 call => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1315 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1316 only_if => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
1319 t8('Save and Invoice'),
1320 call => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1321 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1324 ], # end of combobox "Save"
1332 call => [ 'kivi.Order.show_print_options' ],
1336 call => [ 'kivi.Order.email' ],
1339 t8('Download attachments of all parts'),
1340 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1341 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1342 only_if => $::instance_conf->get_doc_storage,
1344 ], # end of combobox "Export"
1348 call => [ 'kivi.Order.delete_order' ],
1349 confirm => $::locale->text('Do you really want to delete this object?'),
1350 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1351 only_if => $deletion_allowed,
1358 my ($order, $pdf_ref, $params) = @_;
1362 my $print_form = Form->new('');
1363 $print_form->{type} = $order->type;
1364 $print_form->{formname} = $params->{formname} || $order->type;
1365 $print_form->{format} = $params->{format} || 'pdf';
1366 $print_form->{media} = $params->{media} || 'file';
1367 $print_form->{groupitems} = $params->{groupitems};
1368 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1370 $order->language($params->{language});
1371 $order->flatten_to_form($print_form, format_amounts => 1);
1373 # search for the template
1374 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1375 name => $print_form->{formname},
1376 email => $print_form->{media} eq 'email',
1377 language => $params->{language},
1378 printer_id => $print_form->{printer_id}, # todo
1381 if (!defined $template_file) {
1382 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);
1385 return @errors if scalar @errors;
1387 $print_form->throw_on_error(sub {
1389 $print_form->prepare_for_printing;
1391 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1392 template => $template_file,
1393 variables => $print_form,
1394 variable_content_types => {
1395 longdescription => 'html',
1396 partnotes => 'html',
1401 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1407 sub _get_files_for_email_dialog {
1410 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1412 return %files if !$::instance_conf->get_doc_storage;
1414 if ($self->order->id) {
1415 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1416 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1417 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1421 uniq_by { $_->{id} }
1423 +{ id => $_->part->id,
1424 partnumber => $_->part->partnumber }
1425 } @{$self->order->items_sorted};
1427 foreach my $part (@parts) {
1428 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1429 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1432 foreach my $key (keys %files) {
1433 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1439 sub _make_periodic_invoices_config_from_yaml {
1440 my ($yaml_config) = @_;
1442 return if !$yaml_config;
1443 my $attr = YAML::Load($yaml_config);
1444 return if 'HASH' ne ref $attr;
1445 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1449 sub _get_periodic_invoices_status {
1450 my ($self, $config) = @_;
1452 return if $self->type ne _sales_order_type();
1453 return t8('not configured') if !$config;
1455 my $active = ('HASH' eq ref $config) ? $config->{active}
1456 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1457 : die "Cannot get status of periodic invoices config";
1459 return $active ? t8('active') : t8('inactive');
1462 sub _sales_order_type {
1466 sub _purchase_order_type {
1470 sub _sales_quotation_type {
1474 sub _request_quotation_type {
1475 'request_quotation';
1486 SL::Controller::Order - controller for orders
1490 This is a new form to enter orders, completely rewritten with the use
1491 of controller and java script techniques.
1493 The aim is to provide the user a better expirience and a faster flow
1494 of work. Also the code should be more readable, more reliable and
1503 One input row, so that input happens every time at the same place.
1507 Use of pickers where possible.
1511 Possibility to enter more than one item at once.
1515 Save order only on "save" (and "save and delivery order"-workflow). No
1516 hidden save on "print" or "email".
1520 Item list in a scrollable area, so that the workflow buttons stay at
1525 Reordering item rows with drag and drop is possible. Sorting item rows is
1526 possible (by partnumber, description, qty, sellprice and discount for now).
1530 No C<update> is necessary. All entries and calculations are managed
1531 with ajax-calls and the page does only reload on C<save>.
1535 User can see changes immediately, because of the use of java script
1546 =item * C<SL/Controller/Order.pm>
1550 =item * C<template/webpages/order/form.html>
1554 =item * C<template/webpages/order/tabs/basic_data.html>
1556 Main tab for basic_data.
1558 This is the only tab here for now. "linked records" and "webdav" tabs are
1559 reused from generic code.
1563 =item * C<template/webpages/order/tabs/_item_input.html>
1565 The input line for items
1567 =item * C<template/webpages/order/tabs/_row.html>
1569 One row for already entered items
1571 =item * C<template/webpages/order/tabs/_tax_row.html>
1573 Displaying tax information
1575 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1577 Dialog for entering more than one item at once
1579 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1581 Results for the filter in the multi items dialog
1583 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1585 Dialog for selecting price and discount sources
1589 =item * C<js/kivi.Order.js>
1591 java script functions
1603 =item * customer/vendor details ('D'-button)
1605 =item * credit limit
1607 =item * more workflows (save as new, quotation, purchase order)
1609 =item * price sources: little symbols showing better price / better discount
1611 =item * select units in input row?
1613 =item * custom shipto address
1615 =item * language / part translations
1617 =item * access rights
1619 =item * display weights
1625 =item * optional client/user behaviour
1627 (transactions has to be set - department has to be set -
1628 force project if enabled in client config - transport cost reminder)
1632 =head1 KNOWN BUGS AND CAVEATS
1638 Customer discount is not displayed as a valid discount in price source popup
1639 (this might be a bug in price sources)
1641 (I cannot reproduce this (Bernd))
1645 No indication that <shift>-up/down expands/collapses second row.
1649 Inline creation of parts is not currently supported
1653 Table header is not sticky in the scrolling area.
1657 Sorting does not include C<position>, neither does reordering.
1659 This behavior was implemented intentionally. But we can discuss, which behavior
1660 should be implemented.
1664 C<show_multi_items_dialog> does not use the currently inserted string for
1669 The language selected in print or email dialog is not saved when the order is saved.
1673 =head1 To discuss / Nice to have
1679 How to expand/collapse second row. Now it can be done clicking the icon or
1684 Possibility to change longdescription in input row?
1688 Possibility to select PriceSources in input row?
1692 This controller uses a (changed) copy of the template for the PriceSource
1693 dialog. Maybe there could be used one code source.
1697 Rounding-differences between this controller (PriceTaxCalculator) and the old
1698 form. This is not only a problem here, but also in all parts using the PTC.
1699 There exists a ticket and a patch. This patch should be testet.
1703 An indicator, if the actual inputs are saved (like in an
1704 editor or on text processing application).
1708 A warning when leaving the page without saveing unchanged inputs.
1712 Workflows for delivery order and invoice are in the menu "Save", because the
1713 order is saved before opening the new document form. Nevertheless perhaps these
1714 workflow buttons should be put under "Workflows".
1721 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>