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;
20 use SL::DB::RecordLink;
22 use SL::Helper::CreatePDF qw(:all);
23 use SL::Helper::PrintOptions;
25 use SL::Controller::Helper::GetModels;
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);
34 use Rose::Object::MakeMethods::Generic
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) ],
42 __PACKAGE__->run_before('_check_auth');
44 __PACKAGE__->run_before('_recalc',
45 only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
47 __PACKAGE__->run_before('_get_unalterable_data',
48 only => [ qw(save save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
58 $self->order->transdate(DateTime->now_local());
59 $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
64 title => $self->_get_title_for('add'),
65 %{$self->{template_args}}
69 # edit an existing order
78 title => $self->_get_title_for('edit'),
79 %{$self->{template_args}}
87 my $errors = $self->_delete();
89 if (scalar @{ $errors }) {
90 $self->js->flash('error', $_) foreach @{ $errors };
91 return $self->js->render();
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')
99 flash_later('info', $text);
101 my @redirect_params = (
106 $self->redirect_to(@redirect_params);
113 my $errors = $self->_save();
115 if (scalar @{ $errors }) {
116 $self->js->flash('error', $_) foreach @{ $errors };
117 return $self->js->render();
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')
125 flash_later('info', $text);
127 my @redirect_params = (
130 id => $self->order->id,
133 $self->redirect_to(@redirect_params);
138 # This is called if "print" is pressed in the print dialog.
139 # If PDF creation was requested and succeeded, the pdf is stored in a session
140 # file and the filename is stored as session value with an unique key. A
141 # javascript function with this key is then called. This function calls the
142 # download action below (action_download_pdf), which offers the file for
147 my $format = $::form->{print_options}->{format};
148 my $media = $::form->{print_options}->{media};
149 my $formname = $::form->{print_options}->{formname};
150 my $copies = $::form->{print_options}->{copies};
151 my $groupitems = $::form->{print_options}->{groupitems};
154 if (none { $format eq $_ } qw(pdf)) {
155 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
158 # only screen or printer by now
159 if (none { $media eq $_ } qw(screen printer)) {
160 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
164 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
166 # create a form for generate_attachment_filename
167 my $form = Form->new;
168 $form->{ordnumber} = $self->order->ordnumber;
169 $form->{type} = $self->type;
170 $form->{format} = $format;
171 $form->{formname} = $formname;
172 $form->{language} = '_' . $language->template_code if $language;
173 my $pdf_filename = $form->generate_attachment_filename();
176 my @errors = _create_pdf($self->order, \$pdf, { format => $format,
177 formname => $formname,
178 language => $language,
179 groupitems => $groupitems });
180 if (scalar @errors) {
181 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
184 if ($media eq 'screen') {
186 my $sfile = SL::SessionFile::Random->new(mode => "w");
187 $sfile->fh->print($pdf);
190 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
191 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
194 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
195 ->flash('info', t8('The PDF has been created'));
197 } elsif ($media eq 'printer') {
199 my $printer_id = $::form->{print_options}->{printer_id};
200 SL::DB::Printer->new(id => $printer_id)->load->print_document(
205 $self->js->flash('info', t8('The PDF has been printed'));
208 # copy file to webdav folder
209 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
210 my $webdav = SL::Webdav->new(
212 number => $self->order->ordnumber,
214 my $webdav_file = SL::Webdav::File->new(
216 filename => $pdf_filename,
219 $webdav_file->store(data => \$pdf);
222 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
225 if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
227 SL::File->save(object_id => $self->order->id,
228 object_type => $self->type,
229 mime_type => 'application/pdf',
231 file_type => 'document',
232 file_name => $pdf_filename,
233 file_contents => $pdf);
236 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
242 # offer pdf for download
244 # It needs to get the key for the session value to get the pdf file.
245 sub action_download_pdf {
248 my $key = $::form->{key};
249 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
250 return $self->send_file(
252 type => 'application/pdf',
253 name => $::form->{pdf_filename},
257 # open the email dialog
258 sub action_show_email_dialog {
261 my $cv_method = $self->cv;
263 if (!$self->order->$cv_method) {
264 return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
269 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
270 $email_form->{to} ||= $self->order->$cv_method->email;
271 $email_form->{cc} = $self->order->$cv_method->cc;
272 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
273 # Todo: get addresses from shipto, if any
275 my $form = Form->new;
276 $form->{ordnumber} = $self->order->ordnumber;
277 $form->{formname} = $self->type;
278 $form->{type} = $self->type;
279 $form->{language} = 'de';
280 $form->{format} = 'pdf';
282 $email_form->{subject} = $form->generate_email_subject();
283 $email_form->{attachment_filename} = $form->generate_attachment_filename();
284 $email_form->{message} = $form->generate_email_body();
285 $email_form->{js_send_function} = 'kivi.Order.send_email()';
287 my %files = $self->_get_files_for_email_dialog();
288 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
289 email_form => $email_form,
290 show_bcc => $::auth->assert('email_bcc', 'may fail'),
292 is_customer => $self->cv eq 'customer',
296 ->run('kivi.Order.show_email_dialog', $dialog_html)
303 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
304 sub action_send_email {
307 my $email_form = delete $::form->{email_form};
308 my %field_names = (to => 'email');
310 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
312 # for Form::cleanup which may be called in Form::send_email
313 $::form->{cwd} = getcwd();
314 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
316 $::form->{media} = 'email';
318 if (($::form->{attachment_policy} // '') eq 'normal') {
320 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
323 my @errors = _create_pdf($self->order, \$pdf, {media => $::form->{media},
324 format => $::form->{print_options}->{format},
325 formname => $::form->{print_options}->{formname},
326 language => $language,
327 groupitems => $::form->{print_options}->{groupitems}});
328 if (scalar @errors) {
329 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
332 my $sfile = SL::SessionFile::Random->new(mode => "w");
333 $sfile->fh->print($pdf);
336 $::form->{tmpfile} = $sfile->file_name;
337 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
340 $::form->send_email(\%::myconfig, 'pdf');
343 my $intnotes = $self->order->intnotes;
344 $intnotes .= "\n\n" if $self->order->intnotes;
345 $intnotes .= t8('[email]') . "\n";
346 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
347 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
348 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
349 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
350 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
351 $intnotes .= t8('Message') . ": " . $::form->{message};
354 ->val('#order_intnotes', $intnotes)
355 ->run('kivi.Order.close_email_dialog')
356 ->flash('info', t8('The email has been sent.'))
360 # open the periodic invoices config dialog
362 # If there are values in the form (i.e. dialog was opened before),
363 # then use this values. Create new ones, else.
364 sub action_show_periodic_invoices_config_dialog {
367 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
368 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
369 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
370 order_value_periodicity => 'p', # = same as periodicity
371 start_date_as_date => $::form->{transdate} || $::form->current_date,
372 extend_automatically_by => 12,
374 email_subject => GenericTranslations->get(
375 language_id => $::form->{language_id},
376 translation_type =>"preset_text_periodic_invoices_email_subject"),
377 email_body => GenericTranslations->get(
378 language_id => $::form->{language_id},
379 translation_type =>"preset_text_periodic_invoices_email_body"),
381 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
382 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
384 $::form->get_lists(printers => "ALL_PRINTERS",
385 charts => { key => 'ALL_CHARTS',
386 transdate => 'current_date' });
388 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
390 if ($::form->{customer_id}) {
391 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
394 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
396 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
397 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
402 # assign the values of the periodic invoices config dialog
403 # as yaml in the hidden tag and set the status.
404 sub action_assign_periodic_invoices_config {
407 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
409 my $config = { active => $::form->{active} ? 1 : 0,
410 terminated => $::form->{terminated} ? 1 : 0,
411 direct_debit => $::form->{direct_debit} ? 1 : 0,
412 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
413 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
414 start_date_as_date => $::form->{start_date_as_date},
415 end_date_as_date => $::form->{end_date_as_date},
416 first_billing_date_as_date => $::form->{first_billing_date_as_date},
417 print => $::form->{print} ? 1 : 0,
418 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
419 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
420 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
421 ar_chart_id => $::form->{ar_chart_id} * 1,
422 send_email => $::form->{send_email} ? 1 : 0,
423 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
424 email_recipient_address => $::form->{email_recipient_address},
425 email_sender => $::form->{email_sender},
426 email_subject => $::form->{email_subject},
427 email_body => $::form->{email_body},
430 my $periodic_invoices_config = YAML::Dump($config);
432 my $status = $self->_get_periodic_invoices_status($config);
435 ->remove('#order_periodic_invoices_config')
436 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
437 ->run('kivi.Order.close_periodic_invoices_config_dialog')
438 ->html('#periodic_invoices_status', $status)
439 ->flash('info', t8('The periodic invoices config has been assigned.'))
443 sub action_get_has_active_periodic_invoices {
446 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
447 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
449 my $has_active_periodic_invoices =
450 $self->type eq _sales_order_type()
453 && (!$config->end_date || ($config->end_date > DateTime->today_local))
454 && $config->get_previous_billed_period_start_date;
456 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
459 # save the order and redirect to the frontend subroutine for a new
461 sub action_save_and_delivery_order {
464 my $errors = $self->_save();
466 if (scalar @{ $errors }) {
467 $self->js->flash('error', $_) foreach @{ $errors };
468 return $self->js->render();
471 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
472 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
473 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
474 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
476 flash_later('info', $text);
478 my @redirect_params = (
479 controller => 'oe.pl',
480 action => 'oe_delivery_order_from_order',
481 id => $self->order->id,
484 $self->redirect_to(@redirect_params);
487 # save the order and redirect to the frontend subroutine for a new
489 sub action_save_and_invoice {
492 my $errors = $self->_save();
494 if (scalar @{ $errors }) {
495 $self->js->flash('error', $_) foreach @{ $errors };
496 return $self->js->render();
499 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
500 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
501 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
502 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
504 flash_later('info', $text);
506 my @redirect_params = (
507 controller => 'oe.pl',
508 action => 'oe_invoice_from_order',
509 id => $self->order->id,
512 $self->redirect_to(@redirect_params);
515 # workflow from sales quotation to sales order
516 sub action_sales_order {
517 $_[0]->_workflow_sales_or_purchase_order();
520 # workflow from rfq to purchase order
521 sub action_purchase_order {
522 $_[0]->_workflow_sales_or_purchase_order();
525 # set form elements in respect to a changed customer or vendor
527 # This action is called on an change of the customer/vendor picker.
528 sub action_customer_vendor_changed {
531 my $cv_method = $self->cv;
533 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
534 $self->js->show('#cp_row');
536 $self->js->hide('#cp_row');
539 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
540 $self->js->show('#shipto_row');
542 $self->js->hide('#shipto_row');
545 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
547 if ($self->order->is_sales) {
548 $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
549 ? $self->order->$cv_method->taxincluded_checked
550 : $::myconfig{taxincluded_checked});
551 $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
554 $self->order->payment_id($self->order->$cv_method->payment_id);
555 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
560 ->replaceWith('#order_cp_id', $self->build_contact_select)
561 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
562 ->val( '#order_taxzone_id', $self->order->taxzone_id)
563 ->val( '#order_taxincluded', $self->order->taxincluded)
564 ->val( '#order_payment_id', $self->order->payment_id)
565 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
566 ->val( '#order_intnotes', $self->order->$cv_method->notes)
567 ->focus( '#order_' . $self->cv . '_id');
569 $self->_js_redisplay_amounts_and_taxes;
573 # called if a unit in an existing item row is changed
574 sub action_unit_changed {
577 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
578 my $item = $self->order->items_sorted->[$idx];
580 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
581 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
586 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
587 $self->_js_redisplay_line_values;
588 $self->_js_redisplay_amounts_and_taxes;
592 # add an item row for a new item entered in the input row
593 sub action_add_item {
596 my $form_attr = $::form->{add_item};
598 return unless $form_attr->{parts_id};
600 my $item = _new_item($self->order, $form_attr);
602 $self->order->add_items($item);
606 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
607 my $row_as_html = $self->p->render('order/tabs/_row',
611 ALL_PRICE_FACTORS => $self->all_price_factors
615 ->append('#row_table_id', $row_as_html);
617 if ( $item->part->is_assortment ) {
618 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
619 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
620 my $attr = { parts_id => $assortment_item->parts_id,
621 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
622 unit => $assortment_item->unit,
623 description => $assortment_item->part->description,
625 my $item = _new_item($self->order, $attr);
627 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
628 $item->discount(1) unless $assortment_item->charge;
630 $self->order->add_items( $item );
632 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
633 my $row_as_html = $self->p->render('order/tabs/_row',
637 ALL_PRICE_FACTORS => $self->all_price_factors
640 ->append('#row_table_id', $row_as_html);
645 ->val('.add_item_input', '')
646 ->run('kivi.Order.init_row_handlers')
647 ->run('kivi.Order.row_table_scroll_down')
648 ->run('kivi.Order.renumber_positions')
649 ->focus('#add_item_parts_id_name');
651 $self->_js_redisplay_amounts_and_taxes;
655 # open the dialog for entering multiple items at once
656 sub action_show_multi_items_dialog {
657 require SL::DB::PartsGroup;
658 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
659 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
662 # update the filter results in the multi item dialog
663 sub action_multi_items_update_result {
666 $::form->{multi_items}->{filter}->{obsolete} = 0;
668 my $count = $_[0]->multi_items_models->count;
671 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
672 $_[0]->render($text, { layout => 0 });
673 } elsif ($count > $max_count) {
674 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
675 $_[0]->render($text, { layout => 0 });
677 my $multi_items = $_[0]->multi_items_models->get;
678 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
679 multi_items => $multi_items);
683 # add item rows for multiple items at once
684 sub action_add_multi_items {
687 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
688 return $self->js->render() unless scalar @form_attr;
691 foreach my $attr (@form_attr) {
692 my $item = _new_item($self->order, $attr);
694 if ( $item->part->is_assortment ) {
695 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
696 my $attr = { parts_id => $assortment_item->parts_id,
697 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
698 unit => $assortment_item->unit,
699 description => $assortment_item->part->description,
701 my $item = _new_item($self->order, $attr);
703 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
704 $item->discount(1) unless $assortment_item->charge;
709 $self->order->add_items(@items);
713 foreach my $item (@items) {
714 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
715 my $row_as_html = $self->p->render('order/tabs/_row',
719 ALL_PRICE_FACTORS => $self->all_price_factors
722 $self->js->append('#row_table_id', $row_as_html);
726 ->run('kivi.Order.close_multi_items_dialog')
727 ->run('kivi.Order.init_row_handlers')
728 ->run('kivi.Order.row_table_scroll_down')
729 ->run('kivi.Order.renumber_positions')
730 ->focus('#add_item_parts_id_name');
732 $self->_js_redisplay_amounts_and_taxes;
736 # recalculate all linetotals, amounts and taxes and redisplay them
737 sub action_recalc_amounts_and_taxes {
742 $self->_js_redisplay_line_values;
743 $self->_js_redisplay_amounts_and_taxes;
747 # redisplay item rows if they are sorted by an attribute
748 sub action_reorder_items {
752 partnumber => sub { $_[0]->part->partnumber },
753 description => sub { $_[0]->description },
754 qty => sub { $_[0]->qty },
755 sellprice => sub { $_[0]->sellprice },
756 discount => sub { $_[0]->discount },
759 my $method = $sort_keys{$::form->{order_by}};
760 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
761 if ($::form->{sort_dir}) {
762 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
764 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
767 ->run('kivi.Order.redisplay_items', \@to_sort)
771 # show the popup to choose a price/discount source
772 sub action_price_popup {
775 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
776 my $item = $self->order->items_sorted->[$idx];
778 $self->render_price_dialog($item);
781 # get the longdescription for an item if the dialog to enter/change the
782 # longdescription was opened and the longdescription is empty
784 # If this item is new, get the longdescription from Part.
785 # Otherwise get it from OrderItem.
786 sub action_get_item_longdescription {
789 if ($::form->{item_id}) {
790 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
791 } elsif ($::form->{parts_id}) {
792 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
794 $_[0]->render(\ $longdescription, { type => 'text' });
797 # load the second row for one or more items
799 # This action gets the html code for all items second rows by rendering a template for
800 # the second row and sets the html code via client js.
801 sub action_load_second_rows {
804 $self->_recalc() if $self->order->is_sales; # for margin calculation
806 foreach my $item_id (@{ $::form->{item_ids} }) {
807 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
808 my $item = $self->order->items_sorted->[$idx];
810 $self->_js_load_second_row($item, $item_id, 0);
813 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
818 sub _js_load_second_row {
819 my ($self, $item, $item_id, $do_parse) = @_;
822 # Parse values from form (they are formated while rendering (template)).
823 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
824 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
825 foreach my $var (@{ $item->cvars_by_config }) {
826 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
828 $item->parse_custom_variable_values;
831 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
834 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
835 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
838 sub _js_redisplay_line_values {
841 my $is_sales = $self->order->is_sales;
843 # sales orders with margins
848 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
849 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
850 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
851 ]} @{ $self->order->items_sorted };
855 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
856 ]} @{ $self->order->items_sorted };
860 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
863 sub _js_redisplay_amounts_and_taxes {
866 if (scalar @{ $self->{taxes} }) {
867 $self->js->show('#taxincluded_row_id');
869 $self->js->hide('#taxincluded_row_id');
872 if ($self->order->taxincluded) {
873 $self->js->hide('#subtotal_row_id');
875 $self->js->show('#subtotal_row_id');
879 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
880 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
882 ->insertBefore($self->build_tax_rows, '#amount_row_id');
889 sub init_valid_types {
890 [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
896 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
897 die "Not a valid type for order";
900 $self->type($::form->{type});
906 my $cv = (any { $self->type eq $_ } (_sales_order_type(), _sales_quotation_type())) ? 'customer'
907 : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
908 : die "Not a valid type for order";
921 # model used to filter/display the parts in the multi-items dialog
922 sub init_multi_items_models {
923 SL::Controller::Helper::GetModels->new(
926 with_objects => [ qw(unit_obj) ],
927 disable_plugin => 'paginated',
928 source => $::form->{multi_items},
934 partnumber => t8('Partnumber'),
935 description => t8('Description')}
939 sub init_all_price_factors {
940 SL::DB::Manager::PriceFactor->get_all;
946 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
948 my $right = $right_for->{ $self->type };
949 $right ||= 'DOES_NOT_EXIST';
951 $::auth->assert($right);
954 # build the selection box for contacts
956 # Needed, if customer/vendor changed.
957 sub build_contact_select {
960 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
961 value_key => 'cp_id',
962 title_key => 'full_name_dep',
963 default => $self->order->cp_id,
965 style => 'width: 300px',
969 # build the selection box for shiptos
971 # Needed, if customer/vendor changed.
972 sub build_shipto_select {
975 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
976 value_key => 'shipto_id',
977 title_key => 'displayable_id',
978 default => $self->order->shipto_id,
980 style => 'width: 300px',
984 # build the rows for displaying taxes
986 # Called if amounts where recalculated and redisplayed.
991 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
992 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
994 return $rows_as_html;
998 sub render_price_dialog {
999 my ($self, $record_item) = @_;
1001 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1005 'kivi.io.price_chooser_dialog',
1006 t8('Available Prices'),
1007 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1012 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1013 # $self->js->show('#dialog_flash_error');
1022 return if !$::form->{id};
1024 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
1027 # load or create a new order object
1029 # And assign changes from the form to this object.
1030 # If the order is loaded from db, check if items are deleted in the form,
1031 # remove them form the object and collect them for removing from db on saving.
1032 # Then create/update items from form (via _make_item) and add them.
1036 # add_items adds items to an order with no items for saving, but they cannot
1037 # be retrieved via items until the order is saved. Adding empty items to new
1038 # order here solves this problem.
1040 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1041 $order ||= SL::DB::Order->new(orderitems => [],
1042 quotation => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
1044 my $form_orderitems = delete $::form->{order}->{orderitems};
1045 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1047 $order->assign_attributes(%{$::form->{order}});
1049 my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1050 $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1052 # remove deleted items
1053 $self->item_ids_to_delete([]);
1054 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1055 my $item = $order->orderitems->[$idx];
1056 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1057 splice @{$order->orderitems}, $idx, 1;
1058 push @{$self->item_ids_to_delete}, $item->id;
1064 foreach my $form_attr (@{$form_orderitems}) {
1065 my $item = _make_item($order, $form_attr);
1066 $item->position($pos);
1070 $order->add_items(grep {!$_->id} @items);
1075 # create or update items from form
1077 # Make item objects from form values. For items already existing read from db.
1078 # Create a new item else. And assign attributes.
1080 my ($record, $attr) = @_;
1083 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1085 my $is_new = !$item;
1087 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1088 # they cannot be retrieved via custom_variables until the order/orderitem is
1089 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1090 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1092 $item->assign_attributes(%$attr);
1093 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1094 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1095 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1102 # This is used to add one item
1104 my ($record, $attr) = @_;
1106 my $item = SL::DB::OrderItem->new;
1107 $item->assign_attributes(%$attr);
1109 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1110 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1112 $item->unit($part->unit) if !$item->unit;
1115 if ( $part->is_assortment ) {
1116 # add assortment items with price 0, as the components carry the price
1117 $price_src = $price_source->price_from_source("");
1118 $price_src->price(0);
1119 } elsif ($item->sellprice) {
1120 $price_src = $price_source->price_from_source("");
1121 $price_src->price($item->sellprice);
1123 $price_src = $price_source->best_price
1124 ? $price_source->best_price
1125 : $price_source->price_from_source("");
1126 $price_src->price(0) if !$price_source->best_price;
1130 if ($item->discount) {
1131 $discount_src = $price_source->discount_from_source("");
1132 $discount_src->discount($item->discount);
1134 $discount_src = $price_source->best_discount
1135 ? $price_source->best_discount
1136 : $price_source->discount_from_source("");
1137 $discount_src->discount(0) if !$price_source->best_discount;
1141 $new_attr{part} = $part;
1142 $new_attr{description} = $part->description if ! $item->description;
1143 $new_attr{qty} = 1.0 if ! $item->qty;
1144 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1145 $new_attr{sellprice} = $price_src->price;
1146 $new_attr{discount} = $discount_src->discount;
1147 $new_attr{active_price_source} = $price_src;
1148 $new_attr{active_discount_source} = $discount_src;
1149 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1150 $new_attr{project_id} = $record->globalproject_id;
1151 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1153 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1154 # they cannot be retrieved via custom_variables until the order/orderitem is
1155 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1156 $new_attr{custom_variables} = [];
1158 $item->assign_attributes(%new_attr);
1163 # recalculate prices and taxes
1165 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1169 # bb: todo: currency later
1170 $self->order->currency_id($::instance_conf->get_currency_id());
1172 my %pat = $self->order->calculate_prices_and_taxes();
1173 $self->{taxes} = [];
1174 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1175 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1177 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1178 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1179 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1183 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1186 # get data for saving, printing, ..., that is not changed in the form
1188 # Only cvars for now.
1189 sub _get_unalterable_data {
1192 foreach my $item (@{ $self->order->items }) {
1193 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1194 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1195 foreach my $var (@{ $item->cvars_by_config }) {
1196 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1198 $item->parse_custom_variable_values;
1204 # And remove related files in the spool directory
1209 my $db = $self->order->db;
1211 $db->with_transaction(
1213 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1214 $self->order->delete;
1215 my $spool = $::lx_office_conf{paths}->{spool};
1216 unlink map { "$spool/$_" } @spoolfiles if $spool;
1219 }) || push(@{$errors}, $db->error);
1226 # And delete items that are deleted in the form.
1231 my $db = $self->order->db;
1233 $db->with_transaction(sub {
1234 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1235 $self->order->save(cascade => 1);
1238 if ($::form->{converted_from_oe_id}) {
1239 SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
1241 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1243 foreach (@{ $self->order->items_sorted }) {
1244 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1246 SL::DB::RecordLink->new(from_table => 'orderitems',
1247 from_id => $from_id,
1248 to_table => 'orderitems',
1256 }) || push(@{$errors}, $db->error);
1261 sub _workflow_sales_or_purchase_order {
1264 my $destination_type = $::form->{type} eq _sales_quotation_type() ? _sales_order_type()
1265 : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
1268 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1269 $self->{converted_from_oe_id} = delete $::form->{id};
1272 $::form->{type} = $destination_type;
1277 $self->_get_unalterable_data();
1278 $self->_pre_render();
1280 # trigger rendering values for second row/longdescription as hidden,
1281 # because they are loaded only on demand. So we need to keep the values
1283 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1284 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1288 title => $self->_get_title_for('edit'),
1289 %{$self->{template_args}}
1297 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1298 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1299 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1302 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1305 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1307 sort_by => 'projectnumber');
1308 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1310 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1311 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1312 $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
1313 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1315 my $print_form = Form->new('');
1316 $print_form->{type} = $self->type;
1317 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1318 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1319 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1320 form => $print_form,
1321 options => {dialog_name_prefix => 'print_options.',
1325 no_opendocument => 1,
1329 foreach my $item (@{$self->order->orderitems}) {
1330 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1331 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1332 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1335 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1336 my $webdav = SL::Webdav->new(
1337 type => $self->type,
1338 number => $self->order->ordnumber,
1340 my @all_objects = $webdav->get_all_objects;
1341 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1343 link => File::Spec->catfile($_->full_filedescriptor),
1347 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
1348 $self->_setup_edit_action_bar;
1351 sub _setup_edit_action_bar {
1352 my ($self, %params) = @_;
1354 my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
1355 || (($self->type eq _sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1356 || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1358 for my $bar ($::request->layout->get('actionbar')) {
1363 call => [ 'kivi.Order.save', $::instance_conf->get_order_warn_duplicate_parts ],
1364 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1367 t8('Save and Delivery Order'),
1368 call => [ 'kivi.Order.save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1369 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1370 only_if => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
1373 t8('Save and Invoice'),
1374 call => [ 'kivi.Order.save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1375 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1377 ], # end of combobox "Save"
1385 submit => [ '#order_form', { action => "Order/sales_order" } ],
1386 only_if => (any { $self->type eq $_ } (_sales_quotation_type())),
1389 t8('Purchase Order'),
1390 submit => [ '#order_form', { action => "Order/purchase_order" } ],
1391 only_if => (any { $self->type eq $_ } (_request_quotation_type())),
1393 ], # end of combobox "Workflow"
1401 call => [ 'kivi.Order.show_print_options' ],
1405 call => [ 'kivi.Order.email' ],
1408 t8('Download attachments of all parts'),
1409 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1410 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1411 only_if => $::instance_conf->get_doc_storage,
1413 ], # end of combobox "Export"
1417 call => [ 'kivi.Order.delete_order' ],
1418 confirm => $::locale->text('Do you really want to delete this object?'),
1419 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1420 only_if => $deletion_allowed,
1427 my ($order, $pdf_ref, $params) = @_;
1431 my $print_form = Form->new('');
1432 $print_form->{type} = $order->type;
1433 $print_form->{formname} = $params->{formname} || $order->type;
1434 $print_form->{format} = $params->{format} || 'pdf';
1435 $print_form->{media} = $params->{media} || 'file';
1436 $print_form->{groupitems} = $params->{groupitems};
1437 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1439 $order->language($params->{language});
1440 $order->flatten_to_form($print_form, format_amounts => 1);
1442 # search for the template
1443 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1444 name => $print_form->{formname},
1445 email => $print_form->{media} eq 'email',
1446 language => $params->{language},
1447 printer_id => $print_form->{printer_id}, # todo
1450 if (!defined $template_file) {
1451 push @errors, $::locale->text('Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.', join ', ', map { "'$_'"} @template_files);
1454 return @errors if scalar @errors;
1456 $print_form->throw_on_error(sub {
1458 $print_form->prepare_for_printing;
1460 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1461 template => $template_file,
1462 variables => $print_form,
1463 variable_content_types => {
1464 longdescription => 'html',
1465 partnotes => 'html',
1470 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1476 sub _get_files_for_email_dialog {
1479 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1481 return %files if !$::instance_conf->get_doc_storage;
1483 if ($self->order->id) {
1484 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1485 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1486 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1490 uniq_by { $_->{id} }
1492 +{ id => $_->part->id,
1493 partnumber => $_->part->partnumber }
1494 } @{$self->order->items_sorted};
1496 foreach my $part (@parts) {
1497 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1498 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1501 foreach my $key (keys %files) {
1502 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1508 sub _make_periodic_invoices_config_from_yaml {
1509 my ($yaml_config) = @_;
1511 return if !$yaml_config;
1512 my $attr = YAML::Load($yaml_config);
1513 return if 'HASH' ne ref $attr;
1514 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1518 sub _get_periodic_invoices_status {
1519 my ($self, $config) = @_;
1521 return if $self->type ne _sales_order_type();
1522 return t8('not configured') if !$config;
1524 my $active = ('HASH' eq ref $config) ? $config->{active}
1525 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1526 : die "Cannot get status of periodic invoices config";
1528 return $active ? t8('active') : t8('inactive');
1531 sub _get_title_for {
1532 my ($self, $action) = @_;
1534 return '' if none { lc($action)} qw(add edit);
1537 # $::locale->text("Add Sales Order");
1538 # $::locale->text("Add Purchase Order");
1539 # $::locale->text("Add Quotation");
1540 # $::locale->text("Add Request for Quotation");
1541 # $::locale->text("Edit Sales Order");
1542 # $::locale->text("Edit Purchase Order");
1543 # $::locale->text("Edit Quotation");
1544 # $::locale->text("Edit Request for Quotation");
1546 $action = ucfirst(lc($action));
1547 return $self->type eq _sales_order_type() ? $::locale->text("$action Sales Order")
1548 : $self->type eq _purchase_order_type() ? $::locale->text("$action Purchase Order")
1549 : $self->type eq _sales_quotation_type() ? $::locale->text("$action Quotation")
1550 : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
1554 sub _sales_order_type {
1558 sub _purchase_order_type {
1562 sub _sales_quotation_type {
1566 sub _request_quotation_type {
1567 'request_quotation';
1578 SL::Controller::Order - controller for orders
1582 This is a new form to enter orders, completely rewritten with the use
1583 of controller and java script techniques.
1585 The aim is to provide the user a better expirience and a faster flow
1586 of work. Also the code should be more readable, more reliable and
1595 One input row, so that input happens every time at the same place.
1599 Use of pickers where possible.
1603 Possibility to enter more than one item at once.
1607 Save order only on "save" (and "save and delivery order"-workflow). No
1608 hidden save on "print" or "email".
1612 Item list in a scrollable area, so that the workflow buttons stay at
1617 Reordering item rows with drag and drop is possible. Sorting item rows is
1618 possible (by partnumber, description, qty, sellprice and discount for now).
1622 No C<update> is necessary. All entries and calculations are managed
1623 with ajax-calls and the page does only reload on C<save>.
1627 User can see changes immediately, because of the use of java script
1638 =item * C<SL/Controller/Order.pm>
1642 =item * C<template/webpages/order/form.html>
1646 =item * C<template/webpages/order/tabs/basic_data.html>
1648 Main tab for basic_data.
1650 This is the only tab here for now. "linked records" and "webdav" tabs are
1651 reused from generic code.
1655 =item * C<template/webpages/order/tabs/_item_input.html>
1657 The input line for items
1659 =item * C<template/webpages/order/tabs/_row.html>
1661 One row for already entered items
1663 =item * C<template/webpages/order/tabs/_tax_row.html>
1665 Displaying tax information
1667 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1669 Dialog for entering more than one item at once
1671 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1673 Results for the filter in the multi items dialog
1675 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1677 Dialog for selecting price and discount sources
1681 =item * C<js/kivi.Order.js>
1683 java script functions
1695 =item * customer/vendor details ('D'-button)
1697 =item * credit limit
1699 =item * more workflows (save as new, quotation, purchase order)
1701 =item * price sources: little symbols showing better price / better discount
1703 =item * select units in input row?
1705 =item * custom shipto address
1707 =item * language / part translations
1709 =item * access rights
1711 =item * display weights
1717 =item * optional client/user behaviour
1719 (transactions has to be set - department has to be set -
1720 force project if enabled in client config - transport cost reminder)
1724 =head1 KNOWN BUGS AND CAVEATS
1730 Customer discount is not displayed as a valid discount in price source popup
1731 (this might be a bug in price sources)
1733 (I cannot reproduce this (Bernd))
1737 No indication that <shift>-up/down expands/collapses second row.
1741 Inline creation of parts is not currently supported
1745 Table header is not sticky in the scrolling area.
1749 Sorting does not include C<position>, neither does reordering.
1751 This behavior was implemented intentionally. But we can discuss, which behavior
1752 should be implemented.
1756 C<show_multi_items_dialog> does not use the currently inserted string for
1761 The language selected in print or email dialog is not saved when the order is saved.
1765 =head1 To discuss / Nice to have
1771 How to expand/collapse second row. Now it can be done clicking the icon or
1776 Possibility to change longdescription in input row?
1780 Possibility to select PriceSources in input row?
1784 This controller uses a (changed) copy of the template for the PriceSource
1785 dialog. Maybe there could be used one code source.
1789 Rounding-differences between this controller (PriceTaxCalculator) and the old
1790 form. This is not only a problem here, but also in all parts using the PTC.
1791 There exists a ticket and a patch. This patch should be testet.
1795 An indicator, if the actual inputs are saved (like in an
1796 editor or on text processing application).
1800 A warning when leaving the page without saveing unchanged inputs.
1804 Workflows for delivery order and invoice are in the menu "Save", because the
1805 order is saved before opening the new document form. Nevertheless perhaps these
1806 workflow buttons should be put under "Workflows".
1813 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>