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;
13 use SL::Util qw(trim);
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_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
47 __PACKAGE__->run_before('_get_unalterable_data',
48 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print create_pdf send_email) ]);
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);
136 # save the order as new document an open it for edit
137 sub action_save_as_new {
140 if (!$self->order->id) {
141 $self->js->flash('error', t8('This object has not been saved yet.'));
142 return $self->js->render();
145 delete $::form->{$_} for qw(closed delivered converted_from_oe_id converted_from_orderitems_ids);
147 my $src_order = SL::DB::Order->new(id => $self->order->id)->load;
149 # Lets assign a new number if the user hasn't changed the previous one.
150 # If it has been changed manually then use it as-is.
151 if (trim($self->order->number) eq $src_order->number) {
152 $self->order->number('');
155 # Clear reqdate and transdate unless changed
156 if ($self->order->transdate == $src_order->transdate) {
157 $self->order->transdate(DateTime->today_local)
159 if ($self->order->reqdate == $src_order->reqdate) {
160 my $extra_days = $self->type eq _sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
161 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days));
165 $self->order->employee(SL::DB::Manager::Employee->current);
168 $self->action_save();
173 # This is called if "print" is pressed in the print dialog.
174 # If PDF creation was requested and succeeded, the pdf is stored in a session
175 # file and the filename is stored as session value with an unique key. A
176 # javascript function with this key is then called. This function calls the
177 # download action below (action_download_pdf), which offers the file for
182 my $format = $::form->{print_options}->{format};
183 my $media = $::form->{print_options}->{media};
184 my $formname = $::form->{print_options}->{formname};
185 my $copies = $::form->{print_options}->{copies};
186 my $groupitems = $::form->{print_options}->{groupitems};
189 if (none { $format eq $_ } qw(pdf)) {
190 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
193 # only screen or printer by now
194 if (none { $media eq $_ } qw(screen printer)) {
195 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
199 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
201 # create a form for generate_attachment_filename
202 my $form = Form->new;
203 $form->{ordnumber} = $self->order->ordnumber;
204 $form->{type} = $self->type;
205 $form->{format} = $format;
206 $form->{formname} = $formname;
207 $form->{language} = '_' . $language->template_code if $language;
208 my $pdf_filename = $form->generate_attachment_filename();
211 my @errors = _create_pdf($self->order, \$pdf, { format => $format,
212 formname => $formname,
213 language => $language,
214 groupitems => $groupitems });
215 if (scalar @errors) {
216 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
219 if ($media eq 'screen') {
221 my $sfile = SL::SessionFile::Random->new(mode => "w");
222 $sfile->fh->print($pdf);
225 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
226 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
229 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
230 ->flash('info', t8('The PDF has been created'));
232 } elsif ($media eq 'printer') {
234 my $printer_id = $::form->{print_options}->{printer_id};
235 SL::DB::Printer->new(id => $printer_id)->load->print_document(
240 $self->js->flash('info', t8('The PDF has been printed'));
243 # copy file to webdav folder
244 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
245 my $webdav = SL::Webdav->new(
247 number => $self->order->ordnumber,
249 my $webdav_file = SL::Webdav::File->new(
251 filename => $pdf_filename,
254 $webdav_file->store(data => \$pdf);
257 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
260 if ($self->order->ordnumber && $::instance_conf->get_doc_storage) {
262 SL::File->save(object_id => $self->order->id,
263 object_type => $self->type,
264 mime_type => 'application/pdf',
266 file_type => 'document',
267 file_name => $pdf_filename,
268 file_contents => $pdf);
271 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
277 # offer pdf for download
279 # It needs to get the key for the session value to get the pdf file.
280 sub action_download_pdf {
283 my $key = $::form->{key};
284 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
285 return $self->send_file(
287 type => 'application/pdf',
288 name => $::form->{pdf_filename},
292 # open the email dialog
293 sub action_show_email_dialog {
296 my $cv_method = $self->cv;
298 if (!$self->order->$cv_method) {
299 return $self->js->flash('error', $self->cv eq 'customer' ? t8('Cannot send E-mail without customer given') : t8('Cannot send E-mail without vendor given'))
304 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
305 $email_form->{to} ||= $self->order->$cv_method->email;
306 $email_form->{cc} = $self->order->$cv_method->cc;
307 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
308 # Todo: get addresses from shipto, if any
310 my $form = Form->new;
311 $form->{ordnumber} = $self->order->ordnumber;
312 $form->{formname} = $self->type;
313 $form->{type} = $self->type;
314 $form->{language} = 'de';
315 $form->{format} = 'pdf';
317 $email_form->{subject} = $form->generate_email_subject();
318 $email_form->{attachment_filename} = $form->generate_attachment_filename();
319 $email_form->{message} = $form->generate_email_body();
320 $email_form->{js_send_function} = 'kivi.Order.send_email()';
322 my %files = $self->_get_files_for_email_dialog();
323 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
324 email_form => $email_form,
325 show_bcc => $::auth->assert('email_bcc', 'may fail'),
327 is_customer => $self->cv eq 'customer',
331 ->run('kivi.Order.show_email_dialog', $dialog_html)
338 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
339 sub action_send_email {
342 my $email_form = delete $::form->{email_form};
343 my %field_names = (to => 'email');
345 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
347 # for Form::cleanup which may be called in Form::send_email
348 $::form->{cwd} = getcwd();
349 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
351 $::form->{media} = 'email';
353 if (($::form->{attachment_policy} // '') eq 'normal') {
355 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
358 my @errors = _create_pdf($self->order, \$pdf, {media => $::form->{media},
359 format => $::form->{print_options}->{format},
360 formname => $::form->{print_options}->{formname},
361 language => $language,
362 groupitems => $::form->{print_options}->{groupitems}});
363 if (scalar @errors) {
364 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
367 my $sfile = SL::SessionFile::Random->new(mode => "w");
368 $sfile->fh->print($pdf);
371 $::form->{tmpfile} = $sfile->file_name;
372 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
375 $::form->send_email(\%::myconfig, 'pdf');
378 my $intnotes = $self->order->intnotes;
379 $intnotes .= "\n\n" if $self->order->intnotes;
380 $intnotes .= t8('[email]') . "\n";
381 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
382 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
383 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
384 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
385 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
386 $intnotes .= t8('Message') . ": " . $::form->{message};
389 ->val('#order_intnotes', $intnotes)
390 ->run('kivi.Order.close_email_dialog')
391 ->flash('info', t8('The email has been sent.'))
395 # open the periodic invoices config dialog
397 # If there are values in the form (i.e. dialog was opened before),
398 # then use this values. Create new ones, else.
399 sub action_show_periodic_invoices_config_dialog {
402 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
403 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
404 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
405 order_value_periodicity => 'p', # = same as periodicity
406 start_date_as_date => $::form->{transdate} || $::form->current_date,
407 extend_automatically_by => 12,
409 email_subject => GenericTranslations->get(
410 language_id => $::form->{language_id},
411 translation_type =>"preset_text_periodic_invoices_email_subject"),
412 email_body => GenericTranslations->get(
413 language_id => $::form->{language_id},
414 translation_type =>"preset_text_periodic_invoices_email_body"),
416 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
417 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
419 $::form->get_lists(printers => "ALL_PRINTERS",
420 charts => { key => 'ALL_CHARTS',
421 transdate => 'current_date' });
423 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
425 if ($::form->{customer_id}) {
426 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
429 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
431 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
432 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
437 # assign the values of the periodic invoices config dialog
438 # as yaml in the hidden tag and set the status.
439 sub action_assign_periodic_invoices_config {
442 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
444 my $config = { active => $::form->{active} ? 1 : 0,
445 terminated => $::form->{terminated} ? 1 : 0,
446 direct_debit => $::form->{direct_debit} ? 1 : 0,
447 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
448 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
449 start_date_as_date => $::form->{start_date_as_date},
450 end_date_as_date => $::form->{end_date_as_date},
451 first_billing_date_as_date => $::form->{first_billing_date_as_date},
452 print => $::form->{print} ? 1 : 0,
453 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
454 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
455 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
456 ar_chart_id => $::form->{ar_chart_id} * 1,
457 send_email => $::form->{send_email} ? 1 : 0,
458 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
459 email_recipient_address => $::form->{email_recipient_address},
460 email_sender => $::form->{email_sender},
461 email_subject => $::form->{email_subject},
462 email_body => $::form->{email_body},
465 my $periodic_invoices_config = YAML::Dump($config);
467 my $status = $self->_get_periodic_invoices_status($config);
470 ->remove('#order_periodic_invoices_config')
471 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
472 ->run('kivi.Order.close_periodic_invoices_config_dialog')
473 ->html('#periodic_invoices_status', $status)
474 ->flash('info', t8('The periodic invoices config has been assigned.'))
478 sub action_get_has_active_periodic_invoices {
481 my $config = _make_periodic_invoices_config_from_yaml(delete $::form->{config});
482 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
484 my $has_active_periodic_invoices =
485 $self->type eq _sales_order_type()
488 && (!$config->end_date || ($config->end_date > DateTime->today_local))
489 && $config->get_previous_billed_period_start_date;
491 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
494 # save the order and redirect to the frontend subroutine for a new
496 sub action_save_and_delivery_order {
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_delivery_order_from_order',
516 id => $self->order->id,
519 $self->redirect_to(@redirect_params);
522 # save the order and redirect to the frontend subroutine for a new
524 sub action_save_and_invoice {
527 my $errors = $self->_save();
529 if (scalar @{ $errors }) {
530 $self->js->flash('error', $_) foreach @{ $errors };
531 return $self->js->render();
534 my $text = $self->type eq _sales_order_type() ? $::locale->text('The order has been saved')
535 : $self->type eq _purchase_order_type() ? $::locale->text('The order has been saved')
536 : $self->type eq _sales_quotation_type() ? $::locale->text('The quotation has been saved')
537 : $self->type eq _request_quotation_type() ? $::locale->text('The rfq has been saved')
539 flash_later('info', $text);
541 my @redirect_params = (
542 controller => 'oe.pl',
543 action => 'oe_invoice_from_order',
544 id => $self->order->id,
547 $self->redirect_to(@redirect_params);
550 # workflow from sales quotation to sales order
551 sub action_sales_order {
552 $_[0]->_workflow_sales_or_purchase_order();
555 # workflow from rfq to purchase order
556 sub action_purchase_order {
557 $_[0]->_workflow_sales_or_purchase_order();
560 # set form elements in respect to a changed customer or vendor
562 # This action is called on an change of the customer/vendor picker.
563 sub action_customer_vendor_changed {
566 my $cv_method = $self->cv;
568 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
569 $self->js->show('#cp_row');
571 $self->js->hide('#cp_row');
574 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
575 $self->js->show('#shipto_row');
577 $self->js->hide('#shipto_row');
580 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
582 if ($self->order->is_sales) {
583 $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
584 ? $self->order->$cv_method->taxincluded_checked
585 : $::myconfig{taxincluded_checked});
586 $self->js->val('#order_salesman_id', $self->order->$cv_method->salesman_id);
589 $self->order->payment_id($self->order->$cv_method->payment_id);
590 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
595 ->replaceWith('#order_cp_id', $self->build_contact_select)
596 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
597 ->val( '#order_taxzone_id', $self->order->taxzone_id)
598 ->val( '#order_taxincluded', $self->order->taxincluded)
599 ->val( '#order_payment_id', $self->order->payment_id)
600 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
601 ->val( '#order_intnotes', $self->order->$cv_method->notes)
602 ->focus( '#order_' . $self->cv . '_id');
604 $self->_js_redisplay_amounts_and_taxes;
608 # called if a unit in an existing item row is changed
609 sub action_unit_changed {
612 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
613 my $item = $self->order->items_sorted->[$idx];
615 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
616 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
621 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
622 $self->_js_redisplay_line_values;
623 $self->_js_redisplay_amounts_and_taxes;
627 # add an item row for a new item entered in the input row
628 sub action_add_item {
631 my $form_attr = $::form->{add_item};
633 return unless $form_attr->{parts_id};
635 my $item = _new_item($self->order, $form_attr);
637 $self->order->add_items($item);
641 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
642 my $row_as_html = $self->p->render('order/tabs/_row',
646 ALL_PRICE_FACTORS => $self->all_price_factors
650 ->append('#row_table_id', $row_as_html);
652 if ( $item->part->is_assortment ) {
653 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
654 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
655 my $attr = { parts_id => $assortment_item->parts_id,
656 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
657 unit => $assortment_item->unit,
658 description => $assortment_item->part->description,
660 my $item = _new_item($self->order, $attr);
662 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
663 $item->discount(1) unless $assortment_item->charge;
665 $self->order->add_items( $item );
667 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
668 my $row_as_html = $self->p->render('order/tabs/_row',
672 ALL_PRICE_FACTORS => $self->all_price_factors
675 ->append('#row_table_id', $row_as_html);
680 ->val('.add_item_input', '')
681 ->run('kivi.Order.init_row_handlers')
682 ->run('kivi.Order.row_table_scroll_down')
683 ->run('kivi.Order.renumber_positions')
684 ->focus('#add_item_parts_id_name');
686 $self->_js_redisplay_amounts_and_taxes;
690 # open the dialog for entering multiple items at once
691 sub action_show_multi_items_dialog {
692 require SL::DB::PartsGroup;
693 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
694 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
697 # update the filter results in the multi item dialog
698 sub action_multi_items_update_result {
701 $::form->{multi_items}->{filter}->{obsolete} = 0;
703 my $count = $_[0]->multi_items_models->count;
706 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
707 $_[0]->render($text, { layout => 0 });
708 } elsif ($count > $max_count) {
709 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
710 $_[0]->render($text, { layout => 0 });
712 my $multi_items = $_[0]->multi_items_models->get;
713 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
714 multi_items => $multi_items);
718 # add item rows for multiple items at once
719 sub action_add_multi_items {
722 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
723 return $self->js->render() unless scalar @form_attr;
726 foreach my $attr (@form_attr) {
727 my $item = _new_item($self->order, $attr);
729 if ( $item->part->is_assortment ) {
730 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
731 my $attr = { parts_id => $assortment_item->parts_id,
732 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
733 unit => $assortment_item->unit,
734 description => $assortment_item->part->description,
736 my $item = _new_item($self->order, $attr);
738 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
739 $item->discount(1) unless $assortment_item->charge;
744 $self->order->add_items(@items);
748 foreach my $item (@items) {
749 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
750 my $row_as_html = $self->p->render('order/tabs/_row',
754 ALL_PRICE_FACTORS => $self->all_price_factors
757 $self->js->append('#row_table_id', $row_as_html);
761 ->run('kivi.Order.close_multi_items_dialog')
762 ->run('kivi.Order.init_row_handlers')
763 ->run('kivi.Order.row_table_scroll_down')
764 ->run('kivi.Order.renumber_positions')
765 ->focus('#add_item_parts_id_name');
767 $self->_js_redisplay_amounts_and_taxes;
771 # recalculate all linetotals, amounts and taxes and redisplay them
772 sub action_recalc_amounts_and_taxes {
777 $self->_js_redisplay_line_values;
778 $self->_js_redisplay_amounts_and_taxes;
782 # redisplay item rows if they are sorted by an attribute
783 sub action_reorder_items {
787 partnumber => sub { $_[0]->part->partnumber },
788 description => sub { $_[0]->description },
789 qty => sub { $_[0]->qty },
790 sellprice => sub { $_[0]->sellprice },
791 discount => sub { $_[0]->discount },
794 my $method = $sort_keys{$::form->{order_by}};
795 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
796 if ($::form->{sort_dir}) {
797 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
799 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
802 ->run('kivi.Order.redisplay_items', \@to_sort)
806 # show the popup to choose a price/discount source
807 sub action_price_popup {
810 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
811 my $item = $self->order->items_sorted->[$idx];
813 $self->render_price_dialog($item);
816 # get the longdescription for an item if the dialog to enter/change the
817 # longdescription was opened and the longdescription is empty
819 # If this item is new, get the longdescription from Part.
820 # Otherwise get it from OrderItem.
821 sub action_get_item_longdescription {
824 if ($::form->{item_id}) {
825 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
826 } elsif ($::form->{parts_id}) {
827 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
829 $_[0]->render(\ $longdescription, { type => 'text' });
832 # load the second row for one or more items
834 # This action gets the html code for all items second rows by rendering a template for
835 # the second row and sets the html code via client js.
836 sub action_load_second_rows {
839 $self->_recalc() if $self->order->is_sales; # for margin calculation
841 foreach my $item_id (@{ $::form->{item_ids} }) {
842 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
843 my $item = $self->order->items_sorted->[$idx];
845 $self->_js_load_second_row($item, $item_id, 0);
848 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
853 sub _js_load_second_row {
854 my ($self, $item, $item_id, $do_parse) = @_;
857 # Parse values from form (they are formated while rendering (template)).
858 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
859 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
860 foreach my $var (@{ $item->cvars_by_config }) {
861 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
863 $item->parse_custom_variable_values;
866 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
869 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
870 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
873 sub _js_redisplay_line_values {
876 my $is_sales = $self->order->is_sales;
878 # sales orders with margins
883 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
884 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
885 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
886 ]} @{ $self->order->items_sorted };
890 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
891 ]} @{ $self->order->items_sorted };
895 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
898 sub _js_redisplay_amounts_and_taxes {
901 if (scalar @{ $self->{taxes} }) {
902 $self->js->show('#taxincluded_row_id');
904 $self->js->hide('#taxincluded_row_id');
907 if ($self->order->taxincluded) {
908 $self->js->hide('#subtotal_row_id');
910 $self->js->show('#subtotal_row_id');
914 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
915 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
917 ->insertBefore($self->build_tax_rows, '#amount_row_id');
924 sub init_valid_types {
925 [ _sales_order_type(), _purchase_order_type(), _sales_quotation_type(), _request_quotation_type() ];
931 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
932 die "Not a valid type for order";
935 $self->type($::form->{type});
941 my $cv = (any { $self->type eq $_ } (_sales_order_type(), _sales_quotation_type())) ? 'customer'
942 : (any { $self->type eq $_ } (_purchase_order_type(), _request_quotation_type())) ? 'vendor'
943 : die "Not a valid type for order";
956 # model used to filter/display the parts in the multi-items dialog
957 sub init_multi_items_models {
958 SL::Controller::Helper::GetModels->new(
961 with_objects => [ qw(unit_obj) ],
962 disable_plugin => 'paginated',
963 source => $::form->{multi_items},
969 partnumber => t8('Partnumber'),
970 description => t8('Description')}
974 sub init_all_price_factors {
975 SL::DB::Manager::PriceFactor->get_all;
981 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
983 my $right = $right_for->{ $self->type };
984 $right ||= 'DOES_NOT_EXIST';
986 $::auth->assert($right);
989 # build the selection box for contacts
991 # Needed, if customer/vendor changed.
992 sub build_contact_select {
995 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
996 value_key => 'cp_id',
997 title_key => 'full_name_dep',
998 default => $self->order->cp_id,
1000 style => 'width: 300px',
1004 # build the selection box for shiptos
1006 # Needed, if customer/vendor changed.
1007 sub build_shipto_select {
1010 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
1011 value_key => 'shipto_id',
1012 title_key => 'displayable_id',
1013 default => $self->order->shipto_id,
1015 style => 'width: 300px',
1019 # build the rows for displaying taxes
1021 # Called if amounts where recalculated and redisplayed.
1022 sub build_tax_rows {
1026 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1027 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1029 return $rows_as_html;
1033 sub render_price_dialog {
1034 my ($self, $record_item) = @_;
1036 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1040 'kivi.io.price_chooser_dialog',
1041 t8('Available Prices'),
1042 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1047 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1048 # $self->js->show('#dialog_flash_error');
1057 return if !$::form->{id};
1059 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
1062 # load or create a new order object
1064 # And assign changes from the form to this object.
1065 # If the order is loaded from db, check if items are deleted in the form,
1066 # remove them form the object and collect them for removing from db on saving.
1067 # Then create/update items from form (via _make_item) and add them.
1071 # add_items adds items to an order with no items for saving, but they cannot
1072 # be retrieved via items until the order is saved. Adding empty items to new
1073 # order here solves this problem.
1075 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1076 $order ||= SL::DB::Order->new(orderitems => [],
1077 quotation => (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type())));
1079 my $form_orderitems = delete $::form->{order}->{orderitems};
1080 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1082 $order->assign_attributes(%{$::form->{order}});
1084 my $periodic_invoices_config = _make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1085 $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1087 # remove deleted items
1088 $self->item_ids_to_delete([]);
1089 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1090 my $item = $order->orderitems->[$idx];
1091 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1092 splice @{$order->orderitems}, $idx, 1;
1093 push @{$self->item_ids_to_delete}, $item->id;
1099 foreach my $form_attr (@{$form_orderitems}) {
1100 my $item = _make_item($order, $form_attr);
1101 $item->position($pos);
1105 $order->add_items(grep {!$_->id} @items);
1110 # create or update items from form
1112 # Make item objects from form values. For items already existing read from db.
1113 # Create a new item else. And assign attributes.
1115 my ($record, $attr) = @_;
1118 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1120 my $is_new = !$item;
1122 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1123 # they cannot be retrieved via custom_variables until the order/orderitem is
1124 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1125 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1127 $item->assign_attributes(%$attr);
1128 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1129 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1130 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1137 # This is used to add one item
1139 my ($record, $attr) = @_;
1141 my $item = SL::DB::OrderItem->new;
1142 $item->assign_attributes(%$attr);
1144 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1145 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1147 $item->unit($part->unit) if !$item->unit;
1150 if ( $part->is_assortment ) {
1151 # add assortment items with price 0, as the components carry the price
1152 $price_src = $price_source->price_from_source("");
1153 $price_src->price(0);
1154 } elsif ($item->sellprice) {
1155 $price_src = $price_source->price_from_source("");
1156 $price_src->price($item->sellprice);
1158 $price_src = $price_source->best_price
1159 ? $price_source->best_price
1160 : $price_source->price_from_source("");
1161 $price_src->price(0) if !$price_source->best_price;
1165 if ($item->discount) {
1166 $discount_src = $price_source->discount_from_source("");
1167 $discount_src->discount($item->discount);
1169 $discount_src = $price_source->best_discount
1170 ? $price_source->best_discount
1171 : $price_source->discount_from_source("");
1172 $discount_src->discount(0) if !$price_source->best_discount;
1176 $new_attr{part} = $part;
1177 $new_attr{description} = $part->description if ! $item->description;
1178 $new_attr{qty} = 1.0 if ! $item->qty;
1179 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1180 $new_attr{sellprice} = $price_src->price;
1181 $new_attr{discount} = $discount_src->discount;
1182 $new_attr{active_price_source} = $price_src;
1183 $new_attr{active_discount_source} = $discount_src;
1184 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1185 $new_attr{project_id} = $record->globalproject_id;
1186 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1188 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1189 # they cannot be retrieved via custom_variables until the order/orderitem is
1190 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1191 $new_attr{custom_variables} = [];
1193 $item->assign_attributes(%new_attr);
1198 # recalculate prices and taxes
1200 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1204 # bb: todo: currency later
1205 $self->order->currency_id($::instance_conf->get_currency_id());
1207 my %pat = $self->order->calculate_prices_and_taxes();
1208 $self->{taxes} = [];
1209 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1210 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1212 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1213 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1214 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1218 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1221 # get data for saving, printing, ..., that is not changed in the form
1223 # Only cvars for now.
1224 sub _get_unalterable_data {
1227 foreach my $item (@{ $self->order->items }) {
1228 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1229 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1230 foreach my $var (@{ $item->cvars_by_config }) {
1231 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1233 $item->parse_custom_variable_values;
1239 # And remove related files in the spool directory
1244 my $db = $self->order->db;
1246 $db->with_transaction(
1248 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1249 $self->order->delete;
1250 my $spool = $::lx_office_conf{paths}->{spool};
1251 unlink map { "$spool/$_" } @spoolfiles if $spool;
1254 }) || push(@{$errors}, $db->error);
1261 # And delete items that are deleted in the form.
1266 my $db = $self->order->db;
1268 $db->with_transaction(sub {
1269 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1270 $self->order->save(cascade => 1);
1273 if ($::form->{converted_from_oe_id}) {
1274 SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
1276 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1278 foreach (@{ $self->order->items_sorted }) {
1279 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1281 SL::DB::RecordLink->new(from_table => 'orderitems',
1282 from_id => $from_id,
1283 to_table => 'orderitems',
1291 }) || push(@{$errors}, $db->error);
1296 sub _workflow_sales_or_purchase_order {
1299 my $destination_type = $::form->{type} eq _sales_quotation_type() ? _sales_order_type()
1300 : $::form->{type} eq _request_quotation_type() ? _purchase_order_type()
1303 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1304 $self->{converted_from_oe_id} = delete $::form->{id};
1307 $::form->{type} = $destination_type;
1312 $self->_get_unalterable_data();
1313 $self->_pre_render();
1315 # trigger rendering values for second row/longdescription as hidden,
1316 # because they are loaded only on demand. So we need to keep the values
1318 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1319 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1323 title => $self->_get_title_for('edit'),
1324 %{$self->{template_args}}
1332 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1333 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1334 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1337 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1340 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1342 sort_by => 'projectnumber');
1343 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1345 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1346 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1347 $self->{periodic_invoices_status} = $self->_get_periodic_invoices_status($self->order->periodic_invoices_config);
1348 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1350 my $print_form = Form->new('');
1351 $print_form->{type} = $self->type;
1352 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1353 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1354 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1355 form => $print_form,
1356 options => {dialog_name_prefix => 'print_options.',
1360 no_opendocument => 1,
1364 foreach my $item (@{$self->order->orderitems}) {
1365 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1366 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1367 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1370 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1371 my $webdav = SL::Webdav->new(
1372 type => $self->type,
1373 number => $self->order->ordnumber,
1375 my @all_objects = $webdav->get_all_objects;
1376 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1378 link => File::Spec->catfile($_->full_filedescriptor),
1382 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config);
1383 $self->_setup_edit_action_bar;
1386 sub _setup_edit_action_bar {
1387 my ($self, %params) = @_;
1389 my $deletion_allowed = (any { $self->type eq $_ } (_sales_quotation_type(), _request_quotation_type()))
1390 || (($self->type eq _sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1391 || (($self->type eq _purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1393 for my $bar ($::request->layout->get('actionbar')) {
1398 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts ],
1399 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1403 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1404 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1405 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1408 t8('Save and Delivery Order'),
1409 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts ],
1410 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1411 only_if => (any { $self->type eq $_ } (_sales_order_type(), _purchase_order_type()))
1414 t8('Save and Invoice'),
1415 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1416 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1418 ], # end of combobox "Save"
1426 submit => [ '#order_form', { action => "Order/sales_order" } ],
1427 only_if => (any { $self->type eq $_ } (_sales_quotation_type())),
1428 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1431 t8('Purchase Order'),
1432 submit => [ '#order_form', { action => "Order/purchase_order" } ],
1433 only_if => (any { $self->type eq $_ } (_request_quotation_type())),
1434 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1436 ], # end of combobox "Workflow"
1444 call => [ 'kivi.Order.show_print_options' ],
1448 call => [ 'kivi.Order.email' ],
1451 t8('Download attachments of all parts'),
1452 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1453 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1454 only_if => $::instance_conf->get_doc_storage,
1456 ], # end of combobox "Export"
1460 call => [ 'kivi.Order.delete_order' ],
1461 confirm => $::locale->text('Do you really want to delete this object?'),
1462 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1463 only_if => $deletion_allowed,
1470 my ($order, $pdf_ref, $params) = @_;
1474 my $print_form = Form->new('');
1475 $print_form->{type} = $order->type;
1476 $print_form->{formname} = $params->{formname} || $order->type;
1477 $print_form->{format} = $params->{format} || 'pdf';
1478 $print_form->{media} = $params->{media} || 'file';
1479 $print_form->{groupitems} = $params->{groupitems};
1480 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1482 $order->language($params->{language});
1483 $order->flatten_to_form($print_form, format_amounts => 1);
1485 # search for the template
1486 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1487 name => $print_form->{formname},
1488 email => $print_form->{media} eq 'email',
1489 language => $params->{language},
1490 printer_id => $print_form->{printer_id}, # todo
1493 if (!defined $template_file) {
1494 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);
1497 return @errors if scalar @errors;
1499 $print_form->throw_on_error(sub {
1501 $print_form->prepare_for_printing;
1503 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1504 template => $template_file,
1505 variables => $print_form,
1506 variable_content_types => {
1507 longdescription => 'html',
1508 partnotes => 'html',
1513 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1519 sub _get_files_for_email_dialog {
1522 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1524 return %files if !$::instance_conf->get_doc_storage;
1526 if ($self->order->id) {
1527 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1528 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1529 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1533 uniq_by { $_->{id} }
1535 +{ id => $_->part->id,
1536 partnumber => $_->part->partnumber }
1537 } @{$self->order->items_sorted};
1539 foreach my $part (@parts) {
1540 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1541 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1544 foreach my $key (keys %files) {
1545 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1551 sub _make_periodic_invoices_config_from_yaml {
1552 my ($yaml_config) = @_;
1554 return if !$yaml_config;
1555 my $attr = YAML::Load($yaml_config);
1556 return if 'HASH' ne ref $attr;
1557 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1561 sub _get_periodic_invoices_status {
1562 my ($self, $config) = @_;
1564 return if $self->type ne _sales_order_type();
1565 return t8('not configured') if !$config;
1567 my $active = ('HASH' eq ref $config) ? $config->{active}
1568 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1569 : die "Cannot get status of periodic invoices config";
1571 return $active ? t8('active') : t8('inactive');
1574 sub _get_title_for {
1575 my ($self, $action) = @_;
1577 return '' if none { lc($action)} qw(add edit);
1580 # $::locale->text("Add Sales Order");
1581 # $::locale->text("Add Purchase Order");
1582 # $::locale->text("Add Quotation");
1583 # $::locale->text("Add Request for Quotation");
1584 # $::locale->text("Edit Sales Order");
1585 # $::locale->text("Edit Purchase Order");
1586 # $::locale->text("Edit Quotation");
1587 # $::locale->text("Edit Request for Quotation");
1589 $action = ucfirst(lc($action));
1590 return $self->type eq _sales_order_type() ? $::locale->text("$action Sales Order")
1591 : $self->type eq _purchase_order_type() ? $::locale->text("$action Purchase Order")
1592 : $self->type eq _sales_quotation_type() ? $::locale->text("$action Quotation")
1593 : $self->type eq _request_quotation_type() ? $::locale->text("$action Request for Quotation")
1597 sub _sales_order_type {
1601 sub _purchase_order_type {
1605 sub _sales_quotation_type {
1609 sub _request_quotation_type {
1610 'request_quotation';
1621 SL::Controller::Order - controller for orders
1625 This is a new form to enter orders, completely rewritten with the use
1626 of controller and java script techniques.
1628 The aim is to provide the user a better expirience and a faster flow
1629 of work. Also the code should be more readable, more reliable and
1638 One input row, so that input happens every time at the same place.
1642 Use of pickers where possible.
1646 Possibility to enter more than one item at once.
1650 Save order only on "save" (and "save and delivery order"-workflow). No
1651 hidden save on "print" or "email".
1655 Item list in a scrollable area, so that the workflow buttons stay at
1660 Reordering item rows with drag and drop is possible. Sorting item rows is
1661 possible (by partnumber, description, qty, sellprice and discount for now).
1665 No C<update> is necessary. All entries and calculations are managed
1666 with ajax-calls and the page does only reload on C<save>.
1670 User can see changes immediately, because of the use of java script
1681 =item * C<SL/Controller/Order.pm>
1685 =item * C<template/webpages/order/form.html>
1689 =item * C<template/webpages/order/tabs/basic_data.html>
1691 Main tab for basic_data.
1693 This is the only tab here for now. "linked records" and "webdav" tabs are
1694 reused from generic code.
1698 =item * C<template/webpages/order/tabs/_item_input.html>
1700 The input line for items
1702 =item * C<template/webpages/order/tabs/_row.html>
1704 One row for already entered items
1706 =item * C<template/webpages/order/tabs/_tax_row.html>
1708 Displaying tax information
1710 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1712 Dialog for entering more than one item at once
1714 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1716 Results for the filter in the multi items dialog
1718 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1720 Dialog for selecting price and discount sources
1724 =item * C<js/kivi.Order.js>
1726 java script functions
1738 =item * customer/vendor details ('D'-button)
1740 =item * credit limit
1742 =item * more workflows (save as new, quotation, purchase order)
1744 =item * price sources: little symbols showing better price / better discount
1746 =item * select units in input row?
1748 =item * custom shipto address
1750 =item * language / part translations
1752 =item * access rights
1754 =item * display weights
1760 =item * optional client/user behaviour
1762 (transactions has to be set - department has to be set -
1763 force project if enabled in client config - transport cost reminder)
1767 =head1 KNOWN BUGS AND CAVEATS
1773 Customer discount is not displayed as a valid discount in price source popup
1774 (this might be a bug in price sources)
1776 (I cannot reproduce this (Bernd))
1780 No indication that <shift>-up/down expands/collapses second row.
1784 Inline creation of parts is not currently supported
1788 Table header is not sticky in the scrolling area.
1792 Sorting does not include C<position>, neither does reordering.
1794 This behavior was implemented intentionally. But we can discuss, which behavior
1795 should be implemented.
1799 C<show_multi_items_dialog> does not use the currently inserted string for
1804 The language selected in print or email dialog is not saved when the order is saved.
1808 =head1 To discuss / Nice to have
1814 How to expand/collapse second row. Now it can be done clicking the icon or
1819 Possibility to change longdescription in input row?
1823 Possibility to select PriceSources in input row?
1827 This controller uses a (changed) copy of the template for the PriceSource
1828 dialog. Maybe there could be used one code source.
1832 Rounding-differences between this controller (PriceTaxCalculator) and the old
1833 form. This is not only a problem here, but also in all parts using the PTC.
1834 There exists a ticket and a patch. This patch should be testet.
1838 An indicator, if the actual inputs are saved (like in an
1839 editor or on text processing application).
1843 A warning when leaving the page without saveing unchanged inputs.
1847 Workflows for delivery order and invoice are in the menu "Save", because the
1848 order is saved before opening the new document form. Nevertheless perhaps these
1849 workflow buttons should be put under "Workflows".
1856 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>