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);
18 use SL::DB::PartsGroup;
21 use SL::DB::RecordLink;
23 use SL::Helper::CreatePDF qw(:all);
24 use SL::Helper::PrintOptions;
26 use SL::Controller::Helper::GetModels;
28 use List::Util qw(first);
29 use List::UtilsBy qw(sort_by uniq_by);
30 use List::MoreUtils qw(any none pairwise first_index);
31 use English qw(-no_match_vars);
35 use Rose::Object::MakeMethods::Generic
37 scalar => [ qw(item_ids_to_delete) ],
38 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
43 __PACKAGE__->run_before('check_auth');
45 __PACKAGE__->run_before('recalc',
46 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
48 __PACKAGE__->run_before('get_unalterable_data',
49 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
59 $self->order->transdate(DateTime->now_local());
60 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
61 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
66 title => $self->get_title_for('add'),
67 %{$self->{template_args}}
71 # edit an existing order
79 # this is to edit an order from an unsaved order object
81 # set item ids to new fake id, to identify them as new items
82 foreach my $item (@{$self->order->items_sorted}) {
83 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
85 # trigger rendering values for second row/longdescription as hidden,
86 # because they are loaded only on demand. So we need to keep the values
88 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
89 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
96 title => $self->get_title_for('edit'),
97 %{$self->{template_args}}
105 my $errors = $self->delete();
107 if (scalar @{ $errors }) {
108 $self->js->flash('error', $_) foreach @{ $errors };
109 return $self->js->render();
112 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
113 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
114 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
115 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
117 flash_later('info', $text);
119 my @redirect_params = (
124 $self->redirect_to(@redirect_params);
131 my $errors = $self->save();
133 if (scalar @{ $errors }) {
134 $self->js->flash('error', $_) foreach @{ $errors };
135 return $self->js->render();
138 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
139 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
140 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
141 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
143 flash_later('info', $text);
145 my @redirect_params = (
148 id => $self->order->id,
151 $self->redirect_to(@redirect_params);
154 # save the order as new document an open it for edit
155 sub action_save_as_new {
158 my $order = $self->order;
161 $self->js->flash('error', t8('This object has not been saved yet.'));
162 return $self->js->render();
165 # load order from db to check if values changed
166 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
169 # Lets assign a new number if the user hasn't changed the previous one.
170 # If it has been changed manually then use it as-is.
171 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
173 : trim($order->number);
175 # Clear transdate unless changed
176 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
177 ? DateTime->today_local
180 # Set new reqdate unless changed
181 if ($order->reqdate == $saved_order->reqdate) {
182 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
183 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
185 $new_attrs{reqdate} = $order->reqdate;
189 $new_attrs{employee} = SL::DB::Manager::Employee->current;
191 # Create new record from current one
192 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
194 # no linked records on save as new
195 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
198 $self->action_save();
203 # This is called if "print" is pressed in the print dialog.
204 # If PDF creation was requested and succeeded, the pdf is stored in a session
205 # file and the filename is stored as session value with an unique key. A
206 # javascript function with this key is then called. This function calls the
207 # download action below (action_download_pdf), which offers the file for
212 my $format = $::form->{print_options}->{format};
213 my $media = $::form->{print_options}->{media};
214 my $formname = $::form->{print_options}->{formname};
215 my $copies = $::form->{print_options}->{copies};
216 my $groupitems = $::form->{print_options}->{groupitems};
219 if (none { $format eq $_ } qw(pdf)) {
220 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
223 # only screen or printer by now
224 if (none { $media eq $_ } qw(screen printer)) {
225 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
229 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
231 # create a form for generate_attachment_filename
232 my $form = Form->new;
233 $form->{$self->nr_key()} = $self->order->number;
234 $form->{type} = $self->type;
235 $form->{format} = $format;
236 $form->{formname} = $formname;
237 $form->{language} = '_' . $language->template_code if $language;
238 my $pdf_filename = $form->generate_attachment_filename();
241 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
242 formname => $formname,
243 language => $language,
244 groupitems => $groupitems });
245 if (scalar @errors) {
246 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
249 if ($media eq 'screen') {
251 my $sfile = SL::SessionFile::Random->new(mode => "w");
252 $sfile->fh->print($pdf);
255 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
256 $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
259 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
260 ->flash('info', t8('The PDF has been created'));
262 } elsif ($media eq 'printer') {
264 my $printer_id = $::form->{print_options}->{printer_id};
265 SL::DB::Printer->new(id => $printer_id)->load->print_document(
270 $self->js->flash('info', t8('The PDF has been printed'));
273 # copy file to webdav folder
274 if ($self->order->number && $::instance_conf->get_webdav_documents) {
275 my $webdav = SL::Webdav->new(
277 number => $self->order->number,
279 my $webdav_file = SL::Webdav::File->new(
281 filename => $pdf_filename,
284 $webdav_file->store(data => \$pdf);
287 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
290 if ($self->order->number && $::instance_conf->get_doc_storage) {
292 SL::File->save(object_id => $self->order->id,
293 object_type => $self->type,
294 mime_type => 'application/pdf',
296 file_type => 'document',
297 file_name => $pdf_filename,
298 file_contents => $pdf);
301 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
307 # offer pdf for download
309 # It needs to get the key for the session value to get the pdf file.
310 sub action_download_pdf {
313 my $key = $::form->{key};
314 my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
315 return $self->send_file(
317 type => 'application/pdf',
318 name => $::form->{pdf_filename},
322 # open the email dialog
323 sub action_show_email_dialog {
326 my $cv_method = $self->cv;
328 if (!$self->order->$cv_method) {
329 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'))
334 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
335 $email_form->{to} ||= $self->order->$cv_method->email;
336 $email_form->{cc} = $self->order->$cv_method->cc;
337 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
338 # Todo: get addresses from shipto, if any
340 my $form = Form->new;
341 $form->{$self->nr_key()} = $self->order->number;
342 $form->{formname} = $self->type;
343 $form->{type} = $self->type;
344 $form->{language} = 'de';
345 $form->{format} = 'pdf';
347 $email_form->{subject} = $form->generate_email_subject();
348 $email_form->{attachment_filename} = $form->generate_attachment_filename();
349 $email_form->{message} = $form->generate_email_body();
350 $email_form->{js_send_function} = 'kivi.Order.send_email()';
352 my %files = $self->get_files_for_email_dialog();
353 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
354 email_form => $email_form,
355 show_bcc => $::auth->assert('email_bcc', 'may fail'),
357 is_customer => $self->cv eq 'customer',
361 ->run('kivi.Order.show_email_dialog', $dialog_html)
368 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
369 sub action_send_email {
372 my $email_form = delete $::form->{email_form};
373 my %field_names = (to => 'email');
375 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
377 # for Form::cleanup which may be called in Form::send_email
378 $::form->{cwd} = getcwd();
379 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
381 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
382 $::form->{media} = 'email';
384 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
386 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
389 my @errors = genereate_pdf($self->order, \$pdf, {media => $::form->{media},
390 format => $::form->{print_options}->{format},
391 formname => $::form->{print_options}->{formname},
392 language => $language,
393 groupitems => $::form->{print_options}->{groupitems}});
394 if (scalar @errors) {
395 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
398 my $sfile = SL::SessionFile::Random->new(mode => "w");
399 $sfile->fh->print($pdf);
402 $::form->{tmpfile} = $sfile->file_name;
403 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
406 $::form->send_email(\%::myconfig, 'pdf');
409 my $intnotes = $self->order->intnotes;
410 $intnotes .= "\n\n" if $self->order->intnotes;
411 $intnotes .= t8('[email]') . "\n";
412 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
413 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
414 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
415 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
416 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
417 $intnotes .= t8('Message') . ": " . $::form->{message};
420 ->val('#order_intnotes', $intnotes)
421 ->run('kivi.Order.close_email_dialog')
422 ->flash('info', t8('The email has been sent.'))
426 # open the periodic invoices config dialog
428 # If there are values in the form (i.e. dialog was opened before),
429 # then use this values. Create new ones, else.
430 sub action_show_periodic_invoices_config_dialog {
433 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
434 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
435 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
436 order_value_periodicity => 'p', # = same as periodicity
437 start_date_as_date => $::form->{transdate} || $::form->current_date,
438 extend_automatically_by => 12,
440 email_subject => GenericTranslations->get(
441 language_id => $::form->{language_id},
442 translation_type =>"preset_text_periodic_invoices_email_subject"),
443 email_body => GenericTranslations->get(
444 language_id => $::form->{language_id},
445 translation_type =>"preset_text_periodic_invoices_email_body"),
447 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
448 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
450 $::form->get_lists(printers => "ALL_PRINTERS",
451 charts => { key => 'ALL_CHARTS',
452 transdate => 'current_date' });
454 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
456 if ($::form->{customer_id}) {
457 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
460 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
462 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
463 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
468 # assign the values of the periodic invoices config dialog
469 # as yaml in the hidden tag and set the status.
470 sub action_assign_periodic_invoices_config {
473 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
475 my $config = { active => $::form->{active} ? 1 : 0,
476 terminated => $::form->{terminated} ? 1 : 0,
477 direct_debit => $::form->{direct_debit} ? 1 : 0,
478 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
479 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
480 start_date_as_date => $::form->{start_date_as_date},
481 end_date_as_date => $::form->{end_date_as_date},
482 first_billing_date_as_date => $::form->{first_billing_date_as_date},
483 print => $::form->{print} ? 1 : 0,
484 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
485 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
486 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
487 ar_chart_id => $::form->{ar_chart_id} * 1,
488 send_email => $::form->{send_email} ? 1 : 0,
489 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
490 email_recipient_address => $::form->{email_recipient_address},
491 email_sender => $::form->{email_sender},
492 email_subject => $::form->{email_subject},
493 email_body => $::form->{email_body},
496 my $periodic_invoices_config = YAML::Dump($config);
498 my $status = $self->get_periodic_invoices_status($config);
501 ->remove('#order_periodic_invoices_config')
502 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
503 ->run('kivi.Order.close_periodic_invoices_config_dialog')
504 ->html('#periodic_invoices_status', $status)
505 ->flash('info', t8('The periodic invoices config has been assigned.'))
509 sub action_get_has_active_periodic_invoices {
512 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
513 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
515 my $has_active_periodic_invoices =
516 $self->type eq sales_order_type()
519 && (!$config->end_date || ($config->end_date > DateTime->today_local))
520 && $config->get_previous_billed_period_start_date;
522 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
525 # save the order and redirect to the frontend subroutine for a new
527 sub action_save_and_delivery_order {
530 my $errors = $self->save();
532 if (scalar @{ $errors }) {
533 $self->js->flash('error', $_) foreach @{ $errors };
534 return $self->js->render();
537 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
538 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
539 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
540 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
542 flash_later('info', $text);
544 my @redirect_params = (
545 controller => 'oe.pl',
546 action => 'oe_delivery_order_from_order',
547 id => $self->order->id,
550 $self->redirect_to(@redirect_params);
553 # save the order and redirect to the frontend subroutine for a new
555 sub action_save_and_invoice {
558 my $errors = $self->save();
560 if (scalar @{ $errors }) {
561 $self->js->flash('error', $_) foreach @{ $errors };
562 return $self->js->render();
565 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
566 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
567 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
568 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
570 flash_later('info', $text);
572 my @redirect_params = (
573 controller => 'oe.pl',
574 action => 'oe_invoice_from_order',
575 id => $self->order->id,
578 $self->redirect_to(@redirect_params);
581 # workflow from sales quotation to sales order
582 sub action_sales_order {
583 $_[0]->workflow_sales_or_purchase_order();
586 # workflow from rfq to purchase order
587 sub action_purchase_order {
588 $_[0]->workflow_sales_or_purchase_order();
591 # set form elements in respect to a changed customer or vendor
593 # This action is called on an change of the customer/vendor picker.
594 sub action_customer_vendor_changed {
597 setup_order_from_cv($self->order);
600 my $cv_method = $self->cv;
602 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
603 $self->js->show('#cp_row');
605 $self->js->hide('#cp_row');
608 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
609 $self->js->show('#shipto_row');
611 $self->js->hide('#shipto_row');
614 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
617 ->replaceWith('#order_cp_id', $self->build_contact_select)
618 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
619 ->replaceWith('#business_info_row', $self->build_business_info_row)
620 ->val( '#order_taxzone_id', $self->order->taxzone_id)
621 ->val( '#order_taxincluded', $self->order->taxincluded)
622 ->val( '#order_payment_id', $self->order->payment_id)
623 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
624 ->val( '#order_intnotes', $self->order->intnotes)
625 ->focus( '#order_' . $self->cv . '_id');
627 $self->js_redisplay_amounts_and_taxes;
631 # open the dialog for customer/vendor details
632 sub action_show_customer_vendor_details_dialog {
635 my $is_customer = 'customer' eq $::form->{vc};
638 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
640 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
643 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
644 $details{discount_as_percent} = $cv->discount_as_percent;
645 $details{creditlimt} = $cv->creditlimit_as_number;
646 $details{business} = $cv->business->description if $cv->business;
647 $details{language} = $cv->language_obj->description if $cv->language_obj;
648 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
649 $details{payment_terms} = $cv->payment->description if $cv->payment;
650 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
652 foreach my $entry (@{ $cv->shipto }) {
653 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
655 foreach my $entry (@{ $cv->contacts }) {
656 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
659 $_[0]->render('common/show_vc_details', { layout => 0 },
660 is_customer => $is_customer,
665 # called if a unit in an existing item row is changed
666 sub action_unit_changed {
669 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
670 my $item = $self->order->items_sorted->[$idx];
672 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
673 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
678 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
679 $self->js_redisplay_line_values;
680 $self->js_redisplay_amounts_and_taxes;
684 # add an item row for a new item entered in the input row
685 sub action_add_item {
688 my $form_attr = $::form->{add_item};
690 return unless $form_attr->{parts_id};
692 my $item = new_item($self->order, $form_attr);
694 $self->order->add_items($item);
698 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
699 my $row_as_html = $self->p->render('order/tabs/_row',
703 ALL_PRICE_FACTORS => $self->all_price_factors
707 ->append('#row_table_id', $row_as_html);
709 if ( $item->part->is_assortment ) {
710 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
711 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
712 my $attr = { parts_id => $assortment_item->parts_id,
713 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
714 unit => $assortment_item->unit,
715 description => $assortment_item->part->description,
717 my $item = new_item($self->order, $attr);
719 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
720 $item->discount(1) unless $assortment_item->charge;
722 $self->order->add_items( $item );
724 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
725 my $row_as_html = $self->p->render('order/tabs/_row',
729 ALL_PRICE_FACTORS => $self->all_price_factors
732 ->append('#row_table_id', $row_as_html);
737 ->val('.add_item_input', '')
738 ->run('kivi.Order.init_row_handlers')
739 ->run('kivi.Order.row_table_scroll_down')
740 ->run('kivi.Order.renumber_positions')
741 ->focus('#add_item_parts_id_name');
743 $self->js_redisplay_amounts_and_taxes;
747 # open the dialog for entering multiple items at once
748 sub action_show_multi_items_dialog {
749 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
750 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
753 # update the filter results in the multi item dialog
754 sub action_multi_items_update_result {
757 $::form->{multi_items}->{filter}->{obsolete} = 0;
759 my $count = $_[0]->multi_items_models->count;
762 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
763 $_[0]->render($text, { layout => 0 });
764 } elsif ($count > $max_count) {
765 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
766 $_[0]->render($text, { layout => 0 });
768 my $multi_items = $_[0]->multi_items_models->get;
769 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
770 multi_items => $multi_items);
774 # add item rows for multiple items at once
775 sub action_add_multi_items {
778 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
779 return $self->js->render() unless scalar @form_attr;
782 foreach my $attr (@form_attr) {
783 my $item = new_item($self->order, $attr);
785 if ( $item->part->is_assortment ) {
786 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
787 my $attr = { parts_id => $assortment_item->parts_id,
788 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
789 unit => $assortment_item->unit,
790 description => $assortment_item->part->description,
792 my $item = new_item($self->order, $attr);
794 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
795 $item->discount(1) unless $assortment_item->charge;
800 $self->order->add_items(@items);
804 foreach my $item (@items) {
805 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
806 my $row_as_html = $self->p->render('order/tabs/_row',
810 ALL_PRICE_FACTORS => $self->all_price_factors
813 $self->js->append('#row_table_id', $row_as_html);
817 ->run('kivi.Order.close_multi_items_dialog')
818 ->run('kivi.Order.init_row_handlers')
819 ->run('kivi.Order.row_table_scroll_down')
820 ->run('kivi.Order.renumber_positions')
821 ->focus('#add_item_parts_id_name');
823 $self->js_redisplay_amounts_and_taxes;
827 # recalculate all linetotals, amounts and taxes and redisplay them
828 sub action_recalc_amounts_and_taxes {
833 $self->js_redisplay_line_values;
834 $self->js_redisplay_amounts_and_taxes;
838 # redisplay item rows if they are sorted by an attribute
839 sub action_reorder_items {
843 partnumber => sub { $_[0]->part->partnumber },
844 description => sub { $_[0]->description },
845 qty => sub { $_[0]->qty },
846 sellprice => sub { $_[0]->sellprice },
847 discount => sub { $_[0]->discount },
850 my $method = $sort_keys{$::form->{order_by}};
851 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
852 if ($::form->{sort_dir}) {
853 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
855 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
858 ->run('kivi.Order.redisplay_items', \@to_sort)
862 # show the popup to choose a price/discount source
863 sub action_price_popup {
866 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
867 my $item = $self->order->items_sorted->[$idx];
869 $self->render_price_dialog($item);
872 # get the longdescription for an item if the dialog to enter/change the
873 # longdescription was opened and the longdescription is empty
875 # If this item is new, get the longdescription from Part.
876 # Otherwise get it from OrderItem.
877 sub action_get_item_longdescription {
880 if ($::form->{item_id}) {
881 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
882 } elsif ($::form->{parts_id}) {
883 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
885 $_[0]->render(\ $longdescription, { type => 'text' });
888 # load the second row for one or more items
890 # This action gets the html code for all items second rows by rendering a template for
891 # the second row and sets the html code via client js.
892 sub action_load_second_rows {
895 $self->recalc() if $self->order->is_sales; # for margin calculation
897 foreach my $item_id (@{ $::form->{item_ids} }) {
898 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
899 my $item = $self->order->items_sorted->[$idx];
901 $self->js_load_second_row($item, $item_id, 0);
904 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
909 sub js_load_second_row {
910 my ($self, $item, $item_id, $do_parse) = @_;
913 # Parse values from form (they are formated while rendering (template)).
914 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
915 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
916 foreach my $var (@{ $item->cvars_by_config }) {
917 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
919 $item->parse_custom_variable_values;
922 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
925 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
926 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
929 sub js_redisplay_line_values {
932 my $is_sales = $self->order->is_sales;
934 # sales orders with margins
939 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
940 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
941 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
942 ]} @{ $self->order->items_sorted };
946 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
947 ]} @{ $self->order->items_sorted };
951 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
954 sub js_redisplay_amounts_and_taxes {
957 if (scalar @{ $self->{taxes} }) {
958 $self->js->show('#taxincluded_row_id');
960 $self->js->hide('#taxincluded_row_id');
963 if ($self->order->taxincluded) {
964 $self->js->hide('#subtotal_row_id');
966 $self->js->show('#subtotal_row_id');
970 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
971 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
973 ->insertBefore($self->build_tax_rows, '#amount_row_id');
980 sub init_valid_types {
981 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
987 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
988 die "Not a valid type for order";
991 $self->type($::form->{type});
997 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
998 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
999 : die "Not a valid type for order";
1012 # model used to filter/display the parts in the multi-items dialog
1013 sub init_multi_items_models {
1014 SL::Controller::Helper::GetModels->new(
1015 controller => $_[0],
1017 with_objects => [ qw(unit_obj) ],
1018 disable_plugin => 'paginated',
1019 source => $::form->{multi_items},
1025 partnumber => t8('Partnumber'),
1026 description => t8('Description')}
1030 sub init_all_price_factors {
1031 SL::DB::Manager::PriceFactor->get_all;
1037 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1039 my $right = $right_for->{ $self->type };
1040 $right ||= 'DOES_NOT_EXIST';
1042 $::auth->assert($right);
1045 # build the selection box for contacts
1047 # Needed, if customer/vendor changed.
1048 sub build_contact_select {
1051 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1052 value_key => 'cp_id',
1053 title_key => 'full_name_dep',
1054 default => $self->order->cp_id,
1056 style => 'width: 300px',
1060 # build the selection box for shiptos
1062 # Needed, if customer/vendor changed.
1063 sub build_shipto_select {
1066 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
1067 value_key => 'shipto_id',
1068 title_key => 'displayable_id',
1069 default => $self->order->shipto_id,
1071 style => 'width: 300px',
1075 # render the info line for business
1077 # Needed, if customer/vendor changed.
1078 sub build_business_info_row
1080 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1083 # build the rows for displaying taxes
1085 # Called if amounts where recalculated and redisplayed.
1086 sub build_tax_rows {
1090 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1091 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1093 return $rows_as_html;
1097 sub render_price_dialog {
1098 my ($self, $record_item) = @_;
1100 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1104 'kivi.io.price_chooser_dialog',
1105 t8('Available Prices'),
1106 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1111 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1112 # $self->js->show('#dialog_flash_error');
1121 return if !$::form->{id};
1123 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1126 # load or create a new order object
1128 # And assign changes from the form to this object.
1129 # If the order is loaded from db, check if items are deleted in the form,
1130 # remove them form the object and collect them for removing from db on saving.
1131 # Then create/update items from form (via make_item) and add them.
1135 # add_items adds items to an order with no items for saving, but they cannot
1136 # be retrieved via items until the order is saved. Adding empty items to new
1137 # order here solves this problem.
1139 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1140 $order ||= SL::DB::Order->new(orderitems => [],
1141 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
1143 my $cv_id_method = $self->cv . '_id';
1144 if (!$::form->{id} && $::form->{$cv_id_method}) {
1145 $order->$cv_id_method($::form->{$cv_id_method});
1146 setup_order_from_cv($order);
1149 my $form_orderitems = delete $::form->{order}->{orderitems};
1150 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1152 $order->assign_attributes(%{$::form->{order}});
1154 my $periodic_invoices_config = make_periodic_invoices_config_from_yaml($form_periodic_invoices_config);
1155 $order->periodic_invoices_config($periodic_invoices_config) if $periodic_invoices_config;
1157 # remove deleted items
1158 $self->item_ids_to_delete([]);
1159 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1160 my $item = $order->orderitems->[$idx];
1161 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1162 splice @{$order->orderitems}, $idx, 1;
1163 push @{$self->item_ids_to_delete}, $item->id;
1169 foreach my $form_attr (@{$form_orderitems}) {
1170 my $item = make_item($order, $form_attr);
1171 $item->position($pos);
1175 $order->add_items(grep {!$_->id} @items);
1180 # create or update items from form
1182 # Make item objects from form values. For items already existing read from db.
1183 # Create a new item else. And assign attributes.
1185 my ($record, $attr) = @_;
1188 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1190 my $is_new = !$item;
1192 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1193 # they cannot be retrieved via custom_variables until the order/orderitem is
1194 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1195 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1197 $item->assign_attributes(%$attr);
1198 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1199 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1200 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1207 # This is used to add one item
1209 my ($record, $attr) = @_;
1211 my $item = SL::DB::OrderItem->new;
1213 # Remove attributes where the user left or set the inputs empty.
1214 # So these attributes will be undefined and we can distinguish them
1215 # from zero later on.
1216 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1217 delete $attr->{$_} if $attr->{$_} eq '';
1220 $item->assign_attributes(%$attr);
1222 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1223 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1225 $item->unit($part->unit) if !$item->unit;
1228 if ( $part->is_assortment ) {
1229 # add assortment items with price 0, as the components carry the price
1230 $price_src = $price_source->price_from_source("");
1231 $price_src->price(0);
1232 } elsif (defined $item->sellprice) {
1233 $price_src = $price_source->price_from_source("");
1234 $price_src->price($item->sellprice);
1236 $price_src = $price_source->best_price
1237 ? $price_source->best_price
1238 : $price_source->price_from_source("");
1239 $price_src->price(0) if !$price_source->best_price;
1243 if (defined $item->discount) {
1244 $discount_src = $price_source->discount_from_source("");
1245 $discount_src->discount($item->discount);
1247 $discount_src = $price_source->best_discount
1248 ? $price_source->best_discount
1249 : $price_source->discount_from_source("");
1250 $discount_src->discount(0) if !$price_source->best_discount;
1254 $new_attr{part} = $part;
1255 $new_attr{description} = $part->description if ! $item->description;
1256 $new_attr{qty} = 1.0 if ! $item->qty;
1257 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1258 $new_attr{sellprice} = $price_src->price;
1259 $new_attr{discount} = $discount_src->discount;
1260 $new_attr{active_price_source} = $price_src;
1261 $new_attr{active_discount_source} = $discount_src;
1262 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1263 $new_attr{project_id} = $record->globalproject_id;
1264 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1266 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1267 # they cannot be retrieved via custom_variables until the order/orderitem is
1268 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1269 $new_attr{custom_variables} = [];
1271 $item->assign_attributes(%new_attr);
1276 sub setup_order_from_cv {
1279 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
1281 $order->intnotes($order->customervendor->notes);
1283 if ($order->is_sales) {
1284 $order->salesman_id($order->customer->salesman_id);
1285 $order->taxincluded(defined($order->customer->taxincluded_checked)
1286 ? $order->customer->taxincluded_checked
1287 : $::myconfig{taxincluded_checked});
1292 # recalculate prices and taxes
1294 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1298 # bb: todo: currency later
1299 $self->order->currency_id($::instance_conf->get_currency_id());
1301 my %pat = $self->order->calculate_prices_and_taxes();
1302 $self->{taxes} = [];
1303 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1304 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1306 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1307 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1308 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1312 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1315 # get data for saving, printing, ..., that is not changed in the form
1317 # Only cvars for now.
1318 sub get_unalterable_data {
1321 foreach my $item (@{ $self->order->items }) {
1322 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1323 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1324 foreach my $var (@{ $item->cvars_by_config }) {
1325 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1327 $item->parse_custom_variable_values;
1333 # And remove related files in the spool directory
1338 my $db = $self->order->db;
1340 $db->with_transaction(
1342 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1343 $self->order->delete;
1344 my $spool = $::lx_office_conf{paths}->{spool};
1345 unlink map { "$spool/$_" } @spoolfiles if $spool;
1348 }) || push(@{$errors}, $db->error);
1355 # And delete items that are deleted in the form.
1360 my $db = $self->order->db;
1362 $db->with_transaction(sub {
1363 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1364 $self->order->save(cascade => 1);
1367 if ($::form->{converted_from_oe_id}) {
1368 SL::DB::Order->new(id => $::form->{converted_from_oe_id})->load->link_to_record($self->order);
1370 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1372 foreach (@{ $self->order->items_sorted }) {
1373 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1375 SL::DB::RecordLink->new(from_table => 'orderitems',
1376 from_id => $from_id,
1377 to_table => 'orderitems',
1385 }) || push(@{$errors}, $db->error);
1390 sub workflow_sales_or_purchase_order {
1393 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1394 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1395 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1396 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1399 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1400 $self->{converted_from_oe_id} = delete $::form->{id};
1402 # set item ids to new fake id, to identify them as new items
1403 foreach my $item (@{$self->order->items_sorted}) {
1404 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1408 $::form->{type} = $destination_type;
1409 $self->type($self->init_type);
1410 $self->cv ($self->init_cv);
1414 $self->get_unalterable_data();
1415 $self->pre_render();
1417 # trigger rendering values for second row/longdescription as hidden,
1418 # because they are loaded only on demand. So we need to keep the values
1420 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1421 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1425 title => $self->get_title_for('edit'),
1426 %{$self->{template_args}}
1434 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1435 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1436 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1439 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1442 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1444 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1445 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1446 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1447 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1449 my $print_form = Form->new('');
1450 $print_form->{type} = $self->type;
1451 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1452 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1453 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1454 form => $print_form,
1455 options => {dialog_name_prefix => 'print_options.',
1459 no_opendocument => 1,
1463 foreach my $item (@{$self->order->orderitems}) {
1464 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1465 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1466 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1469 if ($self->order->number && $::instance_conf->get_webdav) {
1470 my $webdav = SL::Webdav->new(
1471 type => $self->type,
1472 number => $self->order->number,
1474 my @all_objects = $webdav->get_all_objects;
1475 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1477 link => File::Spec->catfile($_->full_filedescriptor),
1481 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
1482 $self->setup_edit_action_bar;
1485 sub setup_edit_action_bar {
1486 my ($self, %params) = @_;
1488 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
1489 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1490 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1492 for my $bar ($::request->layout->get('actionbar')) {
1497 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1498 $::instance_conf->get_order_warn_no_deliverydate,
1500 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1504 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1505 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1506 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1509 t8('Save and Delivery Order'),
1510 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1511 $::instance_conf->get_order_warn_no_deliverydate,
1513 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1514 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
1517 t8('Save and Invoice'),
1518 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1519 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1521 ], # end of combobox "Save"
1529 submit => [ '#order_form', { action => "Order/sales_order" } ],
1530 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
1531 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1534 t8('Purchase Order'),
1535 submit => [ '#order_form', { action => "Order/purchase_order" } ],
1536 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
1537 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1539 ], # end of combobox "Workflow"
1547 call => [ 'kivi.Order.show_print_options' ],
1551 call => [ 'kivi.Order.email' ],
1554 t8('Download attachments of all parts'),
1555 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1556 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1557 only_if => $::instance_conf->get_doc_storage,
1559 ], # end of combobox "Export"
1563 call => [ 'kivi.Order.delete_order' ],
1564 confirm => $::locale->text('Do you really want to delete this object?'),
1565 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1566 only_if => $deletion_allowed,
1573 my ($order, $pdf_ref, $params) = @_;
1577 my $print_form = Form->new('');
1578 $print_form->{type} = $order->type;
1579 $print_form->{formname} = $params->{formname} || $order->type;
1580 $print_form->{format} = $params->{format} || 'pdf';
1581 $print_form->{media} = $params->{media} || 'file';
1582 $print_form->{groupitems} = $params->{groupitems};
1583 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1585 $order->language($params->{language});
1586 $order->flatten_to_form($print_form, format_amounts => 1);
1588 # search for the template
1589 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1590 name => $print_form->{formname},
1591 email => $print_form->{media} eq 'email',
1592 language => $params->{language},
1593 printer_id => $print_form->{printer_id}, # todo
1596 if (!defined $template_file) {
1597 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);
1600 return @errors if scalar @errors;
1602 $print_form->throw_on_error(sub {
1604 $print_form->prepare_for_printing;
1606 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1607 template => $template_file,
1608 variables => $print_form,
1609 variable_content_types => {
1610 longdescription => 'html',
1611 partnotes => 'html',
1616 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1622 sub get_files_for_email_dialog {
1625 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1627 return %files if !$::instance_conf->get_doc_storage;
1629 if ($self->order->id) {
1630 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1631 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1632 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1636 uniq_by { $_->{id} }
1638 +{ id => $_->part->id,
1639 partnumber => $_->part->partnumber }
1640 } @{$self->order->items_sorted};
1642 foreach my $part (@parts) {
1643 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1644 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1647 foreach my $key (keys %files) {
1648 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1654 sub make_periodic_invoices_config_from_yaml {
1655 my ($yaml_config) = @_;
1657 return if !$yaml_config;
1658 my $attr = YAML::Load($yaml_config);
1659 return if 'HASH' ne ref $attr;
1660 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1664 sub get_periodic_invoices_status {
1665 my ($self, $config) = @_;
1667 return if $self->type ne sales_order_type();
1668 return t8('not configured') if !$config;
1670 my $active = ('HASH' eq ref $config) ? $config->{active}
1671 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1672 : die "Cannot get status of periodic invoices config";
1674 return $active ? t8('active') : t8('inactive');
1678 my ($self, $action) = @_;
1680 return '' if none { lc($action)} qw(add edit);
1683 # $::locale->text("Add Sales Order");
1684 # $::locale->text("Add Purchase Order");
1685 # $::locale->text("Add Quotation");
1686 # $::locale->text("Add Request for Quotation");
1687 # $::locale->text("Edit Sales Order");
1688 # $::locale->text("Edit Purchase Order");
1689 # $::locale->text("Edit Quotation");
1690 # $::locale->text("Edit Request for Quotation");
1692 $action = ucfirst(lc($action));
1693 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
1694 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
1695 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
1696 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
1700 sub sales_order_type {
1704 sub purchase_order_type {
1708 sub sales_quotation_type {
1712 sub request_quotation_type {
1713 'request_quotation';
1717 return $_[0]->type eq sales_order_type() ? 'ordnumber'
1718 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
1719 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
1720 : $_[0]->type eq request_quotation_type() ? 'quonumber'
1732 SL::Controller::Order - controller for orders
1736 This is a new form to enter orders, completely rewritten with the use
1737 of controller and java script techniques.
1739 The aim is to provide the user a better expirience and a faster flow
1740 of work. Also the code should be more readable, more reliable and
1749 One input row, so that input happens every time at the same place.
1753 Use of pickers where possible.
1757 Possibility to enter more than one item at once.
1761 Save order only on "save" (and "save and delivery order"-workflow). No
1762 hidden save on "print" or "email".
1766 Item list in a scrollable area, so that the workflow buttons stay at
1771 Reordering item rows with drag and drop is possible. Sorting item rows is
1772 possible (by partnumber, description, qty, sellprice and discount for now).
1776 No C<update> is necessary. All entries and calculations are managed
1777 with ajax-calls and the page does only reload on C<save>.
1781 User can see changes immediately, because of the use of java script
1792 =item * C<SL/Controller/Order.pm>
1796 =item * C<template/webpages/order/form.html>
1800 =item * C<template/webpages/order/tabs/basic_data.html>
1802 Main tab for basic_data.
1804 This is the only tab here for now. "linked records" and "webdav" tabs are
1805 reused from generic code.
1809 =item * C<template/webpages/order/tabs/_business_info_row.html>
1811 For displaying information on business type
1813 =item * C<template/webpages/order/tabs/_item_input.html>
1815 The input line for items
1817 =item * C<template/webpages/order/tabs/_row.html>
1819 One row for already entered items
1821 =item * C<template/webpages/order/tabs/_tax_row.html>
1823 Displaying tax information
1825 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1827 Dialog for entering more than one item at once
1829 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1831 Results for the filter in the multi items dialog
1833 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1835 Dialog for selecting price and discount sources
1839 =item * C<js/kivi.Order.js>
1841 java script functions
1853 =item * credit limit
1855 =item * more workflows (save as new, quotation, purchase order)
1857 =item * price sources: little symbols showing better price / better discount
1859 =item * select units in input row?
1861 =item * custom shipto address
1863 =item * check for direct delivery (workflow sales order -> purchase order)
1865 =item * language / part translations
1867 =item * access rights
1869 =item * display weights
1875 =item * optional client/user behaviour
1877 (transactions has to be set - department has to be set -
1878 force project if enabled in client config - transport cost reminder)
1882 =head1 KNOWN BUGS AND CAVEATS
1888 Customer discount is not displayed as a valid discount in price source popup
1889 (this might be a bug in price sources)
1891 (I cannot reproduce this (Bernd))
1895 No indication that <shift>-up/down expands/collapses second row.
1899 Inline creation of parts is not currently supported
1903 Table header is not sticky in the scrolling area.
1907 Sorting does not include C<position>, neither does reordering.
1909 This behavior was implemented intentionally. But we can discuss, which behavior
1910 should be implemented.
1914 C<show_multi_items_dialog> does not use the currently inserted string for
1919 The language selected in print or email dialog is not saved when the order is saved.
1923 =head1 To discuss / Nice to have
1929 How to expand/collapse second row. Now it can be done clicking the icon or
1934 Possibility to change longdescription in input row?
1938 Possibility to select PriceSources in input row?
1942 This controller uses a (changed) copy of the template for the PriceSource
1943 dialog. Maybe there could be used one code source.
1947 Rounding-differences between this controller (PriceTaxCalculator) and the old
1948 form. This is not only a problem here, but also in all parts using the PTC.
1949 There exists a ticket and a patch. This patch should be testet.
1953 An indicator, if the actual inputs are saved (like in an
1954 editor or on text processing application).
1958 A warning when leaving the page without saveing unchanged inputs.
1962 Workflows for delivery order and invoice are in the menu "Save", because the
1963 order is saved before opening the new document form. Nevertheless perhaps these
1964 workflow buttons should be put under "Workflows".
1971 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>