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;
25 use SL::Helper::ShippedQty;
27 use SL::Controller::Helper::GetModels;
29 use List::Util qw(first);
30 use List::UtilsBy qw(sort_by uniq_by);
31 use List::MoreUtils qw(any none pairwise first_index);
32 use English qw(-no_match_vars);
36 use Rose::Object::MakeMethods::Generic
38 scalar => [ qw(item_ids_to_delete) ],
39 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
44 __PACKAGE__->run_before('check_auth');
46 __PACKAGE__->run_before('recalc',
47 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
49 __PACKAGE__->run_before('get_unalterable_data',
50 only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
60 $self->order->transdate(DateTime->now_local());
61 my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval :
62 $self->{type} eq 'sales_order' ? $::instance_conf->get_delivery_date_interval : 1;
63 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
69 title => $self->get_title_for('add'),
70 %{$self->{template_args}}
74 # edit an existing order
82 # this is to edit an order from an unsaved order object
84 # set item ids to new fake id, to identify them as new items
85 foreach my $item (@{$self->order->items_sorted}) {
86 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
88 # trigger rendering values for second row/longdescription as hidden,
89 # because they are loaded only on demand. So we need to keep the values
91 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
92 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
99 title => $self->get_title_for('edit'),
100 %{$self->{template_args}}
104 # edit a collective order (consisting of one or more existing orders)
105 sub action_edit_collective {
109 my @multi_ids = map {
110 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
111 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
113 # fall back to add if no ids are given
114 if (scalar @multi_ids == 0) {
119 # fall back to save as new if only one id is given
120 if (scalar @multi_ids == 1) {
121 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
122 $self->action_save_as_new();
126 # make new order from given orders
127 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
128 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
129 $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
131 $self->action_edit();
138 my $errors = $self->delete();
140 if (scalar @{ $errors }) {
141 $self->js->flash('error', $_) foreach @{ $errors };
142 return $self->js->render();
145 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
146 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
147 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
148 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
150 flash_later('info', $text);
152 my @redirect_params = (
157 $self->redirect_to(@redirect_params);
164 my $errors = $self->save();
166 if (scalar @{ $errors }) {
167 $self->js->flash('error', $_) foreach @{ $errors };
168 return $self->js->render();
171 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
172 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
173 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
174 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
176 flash_later('info', $text);
178 my @redirect_params = (
181 id => $self->order->id,
184 $self->redirect_to(@redirect_params);
187 # save the order as new document an open it for edit
188 sub action_save_as_new {
191 my $order = $self->order;
194 $self->js->flash('error', t8('This object has not been saved yet.'));
195 return $self->js->render();
198 # load order from db to check if values changed
199 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
202 # Lets assign a new number if the user hasn't changed the previous one.
203 # If it has been changed manually then use it as-is.
204 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
206 : trim($order->number);
208 # Clear transdate unless changed
209 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
210 ? DateTime->today_local
213 # Set new reqdate unless changed
214 if ($order->reqdate == $saved_order->reqdate) {
215 my $extra_days = $self->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval :
216 $self->{type} eq 'sales_order' ? $::instance_conf->get_delivery_date_interval : 1;
217 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
219 $new_attrs{reqdate} = $order->reqdate;
223 $new_attrs{employee} = SL::DB::Manager::Employee->current;
225 # Create new record from current one
226 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
228 # no linked records on save as new
229 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
232 $self->action_save();
237 # This is called if "print" is pressed in the print dialog.
238 # If PDF creation was requested and succeeded, the pdf is stored in a session
239 # file and the filename is stored as session value with an unique key. A
240 # javascript function with this key is then called. This function calls the
241 # download action below (action_download_pdf), which offers the file for
246 my $errors = $self->save();
248 if (scalar @{ $errors }) {
249 $self->js->flash('error', $_) foreach @{ $errors };
250 return $self->js->render();
253 $self->js->val('#id', $self->order->id)
254 ->val('#order_' . $self->nr_key(), $self->order->number);
256 my $format = $::form->{print_options}->{format};
257 my $media = $::form->{print_options}->{media};
258 my $formname = $::form->{print_options}->{formname};
259 my $copies = $::form->{print_options}->{copies};
260 my $groupitems = $::form->{print_options}->{groupitems};
262 # only pdf and opendocument by now
263 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
264 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
267 # only screen or printer by now
268 if (none { $media eq $_ } qw(screen printer)) {
269 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
273 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
275 # create a form for generate_attachment_filename
276 my $form = Form->new;
277 $form->{$self->nr_key()} = $self->order->number;
278 $form->{type} = $self->type;
279 $form->{format} = $format;
280 $form->{formname} = $formname;
281 $form->{language} = '_' . $language->template_code if $language;
282 my $pdf_filename = $form->generate_attachment_filename();
285 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
286 formname => $formname,
287 language => $language,
288 groupitems => $groupitems });
289 if (scalar @errors) {
290 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
293 if ($media eq 'screen') {
295 my $sfile = SL::SessionFile::Random->new(mode => "w");
296 $sfile->fh->print($pdf);
299 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
300 $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
303 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
304 ->flash('info', t8('The PDF has been created'));
306 } elsif ($media eq 'printer') {
308 my $printer_id = $::form->{print_options}->{printer_id};
309 SL::DB::Printer->new(id => $printer_id)->load->print_document(
314 $self->js->flash('info', t8('The PDF has been printed'));
317 # copy file to webdav folder
318 if ($self->order->number && $::instance_conf->get_webdav_documents) {
319 my $webdav = SL::Webdav->new(
321 number => $self->order->number,
323 my $webdav_file = SL::Webdav::File->new(
325 filename => $pdf_filename,
328 $webdav_file->store(data => \$pdf);
331 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
334 if ($self->order->number && $::instance_conf->get_doc_storage) {
336 SL::File->save(object_id => $self->order->id,
337 object_type => $self->type,
338 mime_type => 'application/pdf',
340 file_type => 'document',
341 file_name => $pdf_filename,
342 file_contents => $pdf);
345 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
351 # offer pdf for download
353 # It needs to get the key for the session value to get the pdf file.
354 sub action_download_pdf {
357 my $key = $::form->{key};
358 my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
359 return $self->send_file(
361 type => 'application/pdf',
362 name => $::form->{pdf_filename},
366 # open the email dialog
367 sub action_show_email_dialog {
370 my $cv_method = $self->cv;
372 if (!$self->order->$cv_method) {
373 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'))
378 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
379 $email_form->{to} ||= $self->order->$cv_method->email;
380 $email_form->{cc} = $self->order->$cv_method->cc;
381 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
382 # Todo: get addresses from shipto, if any
384 my $form = Form->new;
385 $form->{$self->nr_key()} = $self->order->number;
386 $form->{formname} = $self->type;
387 $form->{type} = $self->type;
388 $form->{language} = 'de';
389 $form->{format} = 'pdf';
391 $email_form->{subject} = $form->generate_email_subject();
392 $email_form->{attachment_filename} = $form->generate_attachment_filename();
393 $email_form->{message} = $form->generate_email_body();
394 $email_form->{js_send_function} = 'kivi.Order.send_email()';
396 my %files = $self->get_files_for_email_dialog();
397 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
398 email_form => $email_form,
399 show_bcc => $::auth->assert('email_bcc', 'may fail'),
401 is_customer => $self->cv eq 'customer',
405 ->run('kivi.Order.show_email_dialog', $dialog_html)
412 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
413 sub action_send_email {
416 my $errors = $self->save();
418 if (scalar @{ $errors }) {
419 $self->js->run('kivi.Order.close_email_dialog');
420 $self->js->flash('error', $_) foreach @{ $errors };
421 return $self->js->render();
424 $self->js->val('#id', $self->order->id)
425 ->val('#order_' . $self->nr_key(), $self->order->number);
427 my $email_form = delete $::form->{email_form};
428 my %field_names = (to => 'email');
430 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
432 # for Form::cleanup which may be called in Form::send_email
433 $::form->{cwd} = getcwd();
434 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
436 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
437 $::form->{media} = 'email';
439 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
441 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
444 my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
445 format => $::form->{print_options}->{format},
446 formname => $::form->{print_options}->{formname},
447 language => $language,
448 groupitems => $::form->{print_options}->{groupitems}});
449 if (scalar @errors) {
450 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
453 my $sfile = SL::SessionFile::Random->new(mode => "w");
454 $sfile->fh->print($pdf);
457 $::form->{tmpfile} = $sfile->file_name;
458 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
461 $::form->send_email(\%::myconfig, 'pdf');
464 my $intnotes = $self->order->intnotes;
465 $intnotes .= "\n\n" if $self->order->intnotes;
466 $intnotes .= t8('[email]') . "\n";
467 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
468 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
469 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
470 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
471 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
472 $intnotes .= t8('Message') . ": " . $::form->{message};
474 $self->order->update_attributes(intnotes => $intnotes);
477 ->val('#order_intnotes', $intnotes)
478 ->run('kivi.Order.close_email_dialog')
479 ->flash('info', t8('The email has been sent.'))
483 # open the periodic invoices config dialog
485 # If there are values in the form (i.e. dialog was opened before),
486 # then use this values. Create new ones, else.
487 sub action_show_periodic_invoices_config_dialog {
490 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
491 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
492 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
493 order_value_periodicity => 'p', # = same as periodicity
494 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
495 extend_automatically_by => 12,
497 email_subject => GenericTranslations->get(
498 language_id => $::form->{language_id},
499 translation_type =>"preset_text_periodic_invoices_email_subject"),
500 email_body => GenericTranslations->get(
501 language_id => $::form->{language_id},
502 translation_type =>"preset_text_periodic_invoices_email_body"),
504 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
505 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
507 $::form->get_lists(printers => "ALL_PRINTERS",
508 charts => { key => 'ALL_CHARTS',
509 transdate => 'current_date' });
511 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
513 if ($::form->{customer_id}) {
514 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
515 $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
518 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
520 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
521 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
526 # assign the values of the periodic invoices config dialog
527 # as yaml in the hidden tag and set the status.
528 sub action_assign_periodic_invoices_config {
531 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
533 my $config = { active => $::form->{active} ? 1 : 0,
534 terminated => $::form->{terminated} ? 1 : 0,
535 direct_debit => $::form->{direct_debit} ? 1 : 0,
536 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
537 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
538 start_date_as_date => $::form->{start_date_as_date},
539 end_date_as_date => $::form->{end_date_as_date},
540 first_billing_date_as_date => $::form->{first_billing_date_as_date},
541 print => $::form->{print} ? 1 : 0,
542 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
543 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
544 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
545 ar_chart_id => $::form->{ar_chart_id} * 1,
546 send_email => $::form->{send_email} ? 1 : 0,
547 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
548 email_recipient_address => $::form->{email_recipient_address},
549 email_sender => $::form->{email_sender},
550 email_subject => $::form->{email_subject},
551 email_body => $::form->{email_body},
554 my $periodic_invoices_config = YAML::Dump($config);
556 my $status = $self->get_periodic_invoices_status($config);
559 ->remove('#order_periodic_invoices_config')
560 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
561 ->run('kivi.Order.close_periodic_invoices_config_dialog')
562 ->html('#periodic_invoices_status', $status)
563 ->flash('info', t8('The periodic invoices config has been assigned.'))
567 sub action_get_has_active_periodic_invoices {
570 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
571 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
573 my $has_active_periodic_invoices =
574 $self->type eq sales_order_type()
577 && (!$config->end_date || ($config->end_date > DateTime->today_local))
578 && $config->get_previous_billed_period_start_date;
580 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
583 # save the order and redirect to the frontend subroutine for a new
585 sub action_save_and_delivery_order {
588 my $errors = $self->save();
590 if (scalar @{ $errors }) {
591 $self->js->flash('error', $_) foreach @{ $errors };
592 return $self->js->render();
595 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
596 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
597 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
598 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
600 flash_later('info', $text);
602 my @redirect_params = (
603 controller => 'oe.pl',
604 action => 'oe_delivery_order_from_order',
605 id => $self->order->id,
608 $self->redirect_to(@redirect_params);
611 # save the order and redirect to the frontend subroutine for a new
613 sub action_save_and_invoice {
616 my $errors = $self->save();
618 if (scalar @{ $errors }) {
619 $self->js->flash('error', $_) foreach @{ $errors };
620 return $self->js->render();
623 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
624 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
625 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
626 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
628 flash_later('info', $text);
630 my @redirect_params = (
631 controller => 'oe.pl',
632 action => 'oe_invoice_from_order',
633 id => $self->order->id,
636 $self->redirect_to(@redirect_params);
639 # workflow from sales quotation to sales order
640 sub action_sales_order {
641 $_[0]->workflow_sales_or_purchase_order();
644 # workflow from rfq to purchase order
645 sub action_purchase_order {
646 $_[0]->workflow_sales_or_purchase_order();
649 # set form elements in respect to a changed customer or vendor
651 # This action is called on an change of the customer/vendor picker.
652 sub action_customer_vendor_changed {
655 setup_order_from_cv($self->order);
658 my $cv_method = $self->cv;
660 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
661 $self->js->show('#cp_row');
663 $self->js->hide('#cp_row');
666 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
667 $self->js->show('#shipto_row');
669 $self->js->hide('#shipto_row');
672 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
675 ->replaceWith('#order_cp_id', $self->build_contact_select)
676 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
677 ->replaceWith('#business_info_row', $self->build_business_info_row)
678 ->val( '#order_taxzone_id', $self->order->taxzone_id)
679 ->val( '#order_taxincluded', $self->order->taxincluded)
680 ->val( '#order_payment_id', $self->order->payment_id)
681 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
682 ->val( '#order_intnotes', $self->order->intnotes)
683 ->val( '#language_id', $self->order->$cv_method->language_id)
684 ->focus( '#order_' . $self->cv . '_id');
686 $self->js_redisplay_amounts_and_taxes;
690 # open the dialog for customer/vendor details
691 sub action_show_customer_vendor_details_dialog {
694 my $is_customer = 'customer' eq $::form->{vc};
697 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
699 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
702 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
703 $details{discount_as_percent} = $cv->discount_as_percent;
704 $details{creditlimt} = $cv->creditlimit_as_number;
705 $details{business} = $cv->business->description if $cv->business;
706 $details{language} = $cv->language_obj->description if $cv->language_obj;
707 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
708 $details{payment_terms} = $cv->payment->description if $cv->payment;
709 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
711 foreach my $entry (@{ $cv->shipto }) {
712 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
714 foreach my $entry (@{ $cv->contacts }) {
715 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
718 $_[0]->render('common/show_vc_details', { layout => 0 },
719 is_customer => $is_customer,
724 # called if a unit in an existing item row is changed
725 sub action_unit_changed {
728 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
729 my $item = $self->order->items_sorted->[$idx];
731 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
732 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
737 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
738 $self->js_redisplay_line_values;
739 $self->js_redisplay_amounts_and_taxes;
743 # add an item row for a new item entered in the input row
744 sub action_add_item {
747 my $form_attr = $::form->{add_item};
749 return unless $form_attr->{parts_id};
751 my $item = new_item($self->order, $form_attr);
753 $self->order->add_items($item);
757 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
758 my $row_as_html = $self->p->render('order/tabs/_row',
762 ALL_PRICE_FACTORS => $self->all_price_factors
766 ->append('#row_table_id', $row_as_html);
768 if ( $item->part->is_assortment ) {
769 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
770 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
771 my $attr = { parts_id => $assortment_item->parts_id,
772 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
773 unit => $assortment_item->unit,
774 description => $assortment_item->part->description,
776 my $item = new_item($self->order, $attr);
778 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
779 $item->discount(1) unless $assortment_item->charge;
781 $self->order->add_items( $item );
783 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
784 my $row_as_html = $self->p->render('order/tabs/_row',
788 ALL_PRICE_FACTORS => $self->all_price_factors
791 ->append('#row_table_id', $row_as_html);
796 ->val('.add_item_input', '')
797 ->run('kivi.Order.init_row_handlers')
798 ->run('kivi.Order.row_table_scroll_down')
799 ->run('kivi.Order.renumber_positions')
800 ->focus('#add_item_parts_id_name');
802 $self->js_redisplay_amounts_and_taxes;
806 # open the dialog for entering multiple items at once
807 sub action_show_multi_items_dialog {
808 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
809 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
812 # update the filter results in the multi item dialog
813 sub action_multi_items_update_result {
816 $::form->{multi_items}->{filter}->{obsolete} = 0;
818 my $count = $_[0]->multi_items_models->count;
821 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
822 $_[0]->render($text, { layout => 0 });
823 } elsif ($count > $max_count) {
824 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
825 $_[0]->render($text, { layout => 0 });
827 my $multi_items = $_[0]->multi_items_models->get;
828 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
829 multi_items => $multi_items);
833 # add item rows for multiple items at once
834 sub action_add_multi_items {
837 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
838 return $self->js->render() unless scalar @form_attr;
841 foreach my $attr (@form_attr) {
842 my $item = new_item($self->order, $attr);
844 if ( $item->part->is_assortment ) {
845 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
846 my $attr = { parts_id => $assortment_item->parts_id,
847 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
848 unit => $assortment_item->unit,
849 description => $assortment_item->part->description,
851 my $item = new_item($self->order, $attr);
853 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
854 $item->discount(1) unless $assortment_item->charge;
859 $self->order->add_items(@items);
863 foreach my $item (@items) {
864 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
865 my $row_as_html = $self->p->render('order/tabs/_row',
869 ALL_PRICE_FACTORS => $self->all_price_factors
872 $self->js->append('#row_table_id', $row_as_html);
876 ->run('kivi.Order.close_multi_items_dialog')
877 ->run('kivi.Order.init_row_handlers')
878 ->run('kivi.Order.row_table_scroll_down')
879 ->run('kivi.Order.renumber_positions')
880 ->focus('#add_item_parts_id_name');
882 $self->js_redisplay_amounts_and_taxes;
886 # recalculate all linetotals, amounts and taxes and redisplay them
887 sub action_recalc_amounts_and_taxes {
892 $self->js_redisplay_line_values;
893 $self->js_redisplay_amounts_and_taxes;
897 # redisplay item rows if they are sorted by an attribute
898 sub action_reorder_items {
902 partnumber => sub { $_[0]->part->partnumber },
903 description => sub { $_[0]->description },
904 qty => sub { $_[0]->qty },
905 sellprice => sub { $_[0]->sellprice },
906 discount => sub { $_[0]->discount },
909 my $method = $sort_keys{$::form->{order_by}};
910 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
911 if ($::form->{sort_dir}) {
912 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
914 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
917 ->run('kivi.Order.redisplay_items', \@to_sort)
921 # show the popup to choose a price/discount source
922 sub action_price_popup {
925 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
926 my $item = $self->order->items_sorted->[$idx];
928 $self->render_price_dialog($item);
931 # get the longdescription for an item if the dialog to enter/change the
932 # longdescription was opened and the longdescription is empty
934 # If this item is new, get the longdescription from Part.
935 # Otherwise get it from OrderItem.
936 sub action_get_item_longdescription {
939 if ($::form->{item_id}) {
940 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
941 } elsif ($::form->{parts_id}) {
942 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
944 $_[0]->render(\ $longdescription, { type => 'text' });
947 # load the second row for one or more items
949 # This action gets the html code for all items second rows by rendering a template for
950 # the second row and sets the html code via client js.
951 sub action_load_second_rows {
954 $self->recalc() if $self->order->is_sales; # for margin calculation
956 foreach my $item_id (@{ $::form->{item_ids} }) {
957 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
958 my $item = $self->order->items_sorted->[$idx];
960 $self->js_load_second_row($item, $item_id, 0);
963 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
968 sub js_load_second_row {
969 my ($self, $item, $item_id, $do_parse) = @_;
972 # Parse values from form (they are formated while rendering (template)).
973 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
974 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
975 foreach my $var (@{ $item->cvars_by_config }) {
976 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
978 $item->parse_custom_variable_values;
981 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
984 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
985 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
988 sub js_redisplay_line_values {
991 my $is_sales = $self->order->is_sales;
993 # sales orders with margins
998 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
999 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
1000 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
1001 ]} @{ $self->order->items_sorted };
1005 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1006 ]} @{ $self->order->items_sorted };
1010 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1013 sub js_redisplay_amounts_and_taxes {
1016 if (scalar @{ $self->{taxes} }) {
1017 $self->js->show('#taxincluded_row_id');
1019 $self->js->hide('#taxincluded_row_id');
1022 if ($self->order->taxincluded) {
1023 $self->js->hide('#subtotal_row_id');
1025 $self->js->show('#subtotal_row_id');
1028 if ($self->order->is_sales) {
1029 my $is_neg = $self->order->marge_total < 0;
1031 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1032 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1033 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1034 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1035 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1036 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1037 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1038 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1042 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1043 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1044 ->remove('.tax_row')
1045 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1052 sub init_valid_types {
1053 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1059 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1060 die "Not a valid type for order";
1063 $self->type($::form->{type});
1069 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1070 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1071 : die "Not a valid type for order";
1084 # model used to filter/display the parts in the multi-items dialog
1085 sub init_multi_items_models {
1086 SL::Controller::Helper::GetModels->new(
1087 controller => $_[0],
1089 with_objects => [ qw(unit_obj) ],
1090 disable_plugin => 'paginated',
1091 source => $::form->{multi_items},
1097 partnumber => t8('Partnumber'),
1098 description => t8('Description')}
1102 sub init_all_price_factors {
1103 SL::DB::Manager::PriceFactor->get_all;
1109 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1111 my $right = $right_for->{ $self->type };
1112 $right ||= 'DOES_NOT_EXIST';
1114 $::auth->assert($right);
1117 # build the selection box for contacts
1119 # Needed, if customer/vendor changed.
1120 sub build_contact_select {
1123 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1124 value_key => 'cp_id',
1125 title_key => 'full_name_dep',
1126 default => $self->order->cp_id,
1128 style => 'width: 300px',
1132 # build the selection box for shiptos
1134 # Needed, if customer/vendor changed.
1135 sub build_shipto_select {
1138 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
1139 value_key => 'shipto_id',
1140 title_key => 'displayable_id',
1141 default => $self->order->shipto_id,
1143 style => 'width: 300px',
1147 # render the info line for business
1149 # Needed, if customer/vendor changed.
1150 sub build_business_info_row
1152 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1155 # build the rows for displaying taxes
1157 # Called if amounts where recalculated and redisplayed.
1158 sub build_tax_rows {
1162 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1163 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1165 return $rows_as_html;
1169 sub render_price_dialog {
1170 my ($self, $record_item) = @_;
1172 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1176 'kivi.io.price_chooser_dialog',
1177 t8('Available Prices'),
1178 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1183 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1184 # $self->js->show('#dialog_flash_error');
1193 return if !$::form->{id};
1195 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1198 # load or create a new order object
1200 # And assign changes from the form to this object.
1201 # If the order is loaded from db, check if items are deleted in the form,
1202 # remove them form the object and collect them for removing from db on saving.
1203 # Then create/update items from form (via make_item) and add them.
1207 # add_items adds items to an order with no items for saving, but they cannot
1208 # be retrieved via items until the order is saved. Adding empty items to new
1209 # order here solves this problem.
1211 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1212 $order ||= SL::DB::Order->new(orderitems => [],
1213 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
1215 my $cv_id_method = $self->cv . '_id';
1216 if (!$::form->{id} && $::form->{$cv_id_method}) {
1217 $order->$cv_id_method($::form->{$cv_id_method});
1218 setup_order_from_cv($order);
1221 my $form_orderitems = delete $::form->{order}->{orderitems};
1222 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1224 $order->assign_attributes(%{$::form->{order}});
1226 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? YAML::Load($form_periodic_invoices_config) : undef) {
1227 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1228 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1231 # remove deleted items
1232 $self->item_ids_to_delete([]);
1233 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1234 my $item = $order->orderitems->[$idx];
1235 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1236 splice @{$order->orderitems}, $idx, 1;
1237 push @{$self->item_ids_to_delete}, $item->id;
1243 foreach my $form_attr (@{$form_orderitems}) {
1244 my $item = make_item($order, $form_attr);
1245 $item->position($pos);
1249 $order->add_items(grep {!$_->id} @items);
1254 # create or update items from form
1256 # Make item objects from form values. For items already existing read from db.
1257 # Create a new item else. And assign attributes.
1259 my ($record, $attr) = @_;
1262 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1264 my $is_new = !$item;
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 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1271 $item->assign_attributes(%$attr);
1272 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1273 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1274 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1281 # This is used to add one item
1283 my ($record, $attr) = @_;
1285 my $item = SL::DB::OrderItem->new;
1287 # Remove attributes where the user left or set the inputs empty.
1288 # So these attributes will be undefined and we can distinguish them
1289 # from zero later on.
1290 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1291 delete $attr->{$_} if $attr->{$_} eq '';
1294 $item->assign_attributes(%$attr);
1296 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1297 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1299 $item->unit($part->unit) if !$item->unit;
1302 if ( $part->is_assortment ) {
1303 # add assortment items with price 0, as the components carry the price
1304 $price_src = $price_source->price_from_source("");
1305 $price_src->price(0);
1306 } elsif (defined $item->sellprice) {
1307 $price_src = $price_source->price_from_source("");
1308 $price_src->price($item->sellprice);
1310 $price_src = $price_source->best_price
1311 ? $price_source->best_price
1312 : $price_source->price_from_source("");
1313 $price_src->price(0) if !$price_source->best_price;
1317 if (defined $item->discount) {
1318 $discount_src = $price_source->discount_from_source("");
1319 $discount_src->discount($item->discount);
1321 $discount_src = $price_source->best_discount
1322 ? $price_source->best_discount
1323 : $price_source->discount_from_source("");
1324 $discount_src->discount(0) if !$price_source->best_discount;
1328 $new_attr{part} = $part;
1329 $new_attr{description} = $part->description if ! $item->description;
1330 $new_attr{qty} = 1.0 if ! $item->qty;
1331 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1332 $new_attr{sellprice} = $price_src->price;
1333 $new_attr{discount} = $discount_src->discount;
1334 $new_attr{active_price_source} = $price_src;
1335 $new_attr{active_discount_source} = $discount_src;
1336 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1337 $new_attr{project_id} = $record->globalproject_id;
1338 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1340 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1341 # they cannot be retrieved via custom_variables until the order/orderitem is
1342 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1343 $new_attr{custom_variables} = [];
1345 $item->assign_attributes(%new_attr);
1350 sub setup_order_from_cv {
1353 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
1355 $order->intnotes($order->customervendor->notes);
1357 if ($order->is_sales) {
1358 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1359 $order->taxincluded(defined($order->customer->taxincluded_checked)
1360 ? $order->customer->taxincluded_checked
1361 : $::myconfig{taxincluded_checked});
1366 # recalculate prices and taxes
1368 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1372 # bb: todo: currency later
1373 $self->order->currency_id($::instance_conf->get_currency_id());
1375 my %pat = $self->order->calculate_prices_and_taxes();
1376 $self->{taxes} = [];
1377 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1378 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1380 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1381 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1382 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1386 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1389 # get data for saving, printing, ..., that is not changed in the form
1391 # Only cvars for now.
1392 sub get_unalterable_data {
1395 foreach my $item (@{ $self->order->items }) {
1396 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1397 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1398 foreach my $var (@{ $item->cvars_by_config }) {
1399 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1401 $item->parse_custom_variable_values;
1407 # And remove related files in the spool directory
1412 my $db = $self->order->db;
1414 $db->with_transaction(
1416 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1417 $self->order->delete;
1418 my $spool = $::lx_office_conf{paths}->{spool};
1419 unlink map { "$spool/$_" } @spoolfiles if $spool;
1422 }) || push(@{$errors}, $db->error);
1429 # And delete items that are deleted in the form.
1434 my $db = $self->order->db;
1436 $db->with_transaction(sub {
1437 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1438 $self->order->save(cascade => 1);
1441 if ($::form->{converted_from_oe_id}) {
1442 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1443 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1444 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1445 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1446 $src->link_to_record($self->order);
1448 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1450 foreach (@{ $self->order->items_sorted }) {
1451 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1453 SL::DB::RecordLink->new(from_table => 'orderitems',
1454 from_id => $from_id,
1455 to_table => 'orderitems',
1463 }) || push(@{$errors}, $db->error);
1468 sub workflow_sales_or_purchase_order {
1471 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1472 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1473 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1474 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1477 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1478 $self->{converted_from_oe_id} = delete $::form->{id};
1480 # set item ids to new fake id, to identify them as new items
1481 foreach my $item (@{$self->order->items_sorted}) {
1482 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1486 $::form->{type} = $destination_type;
1487 $self->type($self->init_type);
1488 $self->cv ($self->init_cv);
1492 $self->get_unalterable_data();
1493 $self->pre_render();
1495 # trigger rendering values for second row/longdescription as hidden,
1496 # because they are loaded only on demand. So we need to keep the values
1498 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1499 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1503 title => $self->get_title_for('edit'),
1504 %{$self->{template_args}}
1512 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1513 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1514 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1517 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1520 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1522 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1523 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1524 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1525 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1527 my $print_form = Form->new('');
1528 $print_form->{type} = $self->type;
1529 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1530 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1531 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1532 form => $print_form,
1533 options => {dialog_name_prefix => 'print_options.',
1537 no_opendocument => 0,
1541 foreach my $item (@{$self->order->orderitems}) {
1542 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1543 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1544 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1547 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
1548 # calculate shipped qtys here to prevent calling calculate for every item via the items method
1549 SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
1552 if ($self->order->number && $::instance_conf->get_webdav) {
1553 my $webdav = SL::Webdav->new(
1554 type => $self->type,
1555 number => $self->order->number,
1557 my @all_objects = $webdav->get_all_objects;
1558 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1560 link => File::Spec->catfile($_->full_filedescriptor),
1564 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
1565 $self->setup_edit_action_bar;
1568 sub setup_edit_action_bar {
1569 my ($self, %params) = @_;
1571 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
1572 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1573 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1575 for my $bar ($::request->layout->get('actionbar')) {
1580 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1581 $::instance_conf->get_order_warn_no_deliverydate,
1583 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1587 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1588 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1589 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1591 ], # end of combobox "Save"
1599 submit => [ '#order_form', { action => "Order/sales_order" } ],
1600 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
1601 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1604 t8('Purchase Order'),
1605 submit => [ '#order_form', { action => "Order/purchase_order" } ],
1606 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
1607 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1610 t8('Save and Delivery Order'),
1611 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1612 $::instance_conf->get_order_warn_no_deliverydate,
1614 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1615 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
1618 t8('Save and Invoice'),
1619 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1620 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1622 ], # end of combobox "Workflow"
1629 t8('Save and print'),
1630 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
1633 t8('Save and E-mail'),
1634 call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
1637 t8('Download attachments of all parts'),
1638 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1639 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1640 only_if => $::instance_conf->get_doc_storage,
1642 ], # end of combobox "Export"
1646 call => [ 'kivi.Order.delete_order' ],
1647 confirm => $::locale->text('Do you really want to delete this object?'),
1648 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1649 only_if => $deletion_allowed,
1656 my ($order, $pdf_ref, $params) = @_;
1660 my $print_form = Form->new('');
1661 $print_form->{type} = $order->type;
1662 $print_form->{formname} = $params->{formname} || $order->type;
1663 $print_form->{format} = $params->{format} || 'pdf';
1664 $print_form->{media} = $params->{media} || 'file';
1665 $print_form->{groupitems} = $params->{groupitems};
1666 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1668 $order->language($params->{language});
1669 $order->flatten_to_form($print_form, format_amounts => 1);
1673 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
1674 $template_ext = 'odt';
1675 $template_type = 'OpenDocument';
1678 # search for the template
1679 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1680 name => $print_form->{formname},
1681 extension => $template_ext,
1682 email => $print_form->{media} eq 'email',
1683 language => $params->{language},
1684 printer_id => $print_form->{printer_id}, # todo
1687 if (!defined $template_file) {
1688 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);
1691 return @errors if scalar @errors;
1693 $print_form->throw_on_error(sub {
1695 $print_form->prepare_for_printing;
1697 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1698 format => $print_form->{format},
1699 template_type => $template_type,
1700 template => $template_file,
1701 variables => $print_form,
1702 variable_content_types => {
1703 longdescription => 'html',
1704 partnotes => 'html',
1709 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1715 sub get_files_for_email_dialog {
1718 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1720 return %files if !$::instance_conf->get_doc_storage;
1722 if ($self->order->id) {
1723 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1724 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1725 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1729 uniq_by { $_->{id} }
1731 +{ id => $_->part->id,
1732 partnumber => $_->part->partnumber }
1733 } @{$self->order->items_sorted};
1735 foreach my $part (@parts) {
1736 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1737 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1740 foreach my $key (keys %files) {
1741 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1747 sub make_periodic_invoices_config_from_yaml {
1748 my ($yaml_config) = @_;
1750 return if !$yaml_config;
1751 my $attr = YAML::Load($yaml_config);
1752 return if 'HASH' ne ref $attr;
1753 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1757 sub get_periodic_invoices_status {
1758 my ($self, $config) = @_;
1760 return if $self->type ne sales_order_type();
1761 return t8('not configured') if !$config;
1763 my $active = ('HASH' eq ref $config) ? $config->{active}
1764 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1765 : die "Cannot get status of periodic invoices config";
1767 return $active ? t8('active') : t8('inactive');
1771 my ($self, $action) = @_;
1773 return '' if none { lc($action)} qw(add edit);
1776 # $::locale->text("Add Sales Order");
1777 # $::locale->text("Add Purchase Order");
1778 # $::locale->text("Add Quotation");
1779 # $::locale->text("Add Request for Quotation");
1780 # $::locale->text("Edit Sales Order");
1781 # $::locale->text("Edit Purchase Order");
1782 # $::locale->text("Edit Quotation");
1783 # $::locale->text("Edit Request for Quotation");
1785 $action = ucfirst(lc($action));
1786 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
1787 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
1788 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
1789 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
1793 sub sales_order_type {
1797 sub purchase_order_type {
1801 sub sales_quotation_type {
1805 sub request_quotation_type {
1806 'request_quotation';
1810 return $_[0]->type eq sales_order_type() ? 'ordnumber'
1811 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
1812 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
1813 : $_[0]->type eq request_quotation_type() ? 'quonumber'
1825 SL::Controller::Order - controller for orders
1829 This is a new form to enter orders, completely rewritten with the use
1830 of controller and java script techniques.
1832 The aim is to provide the user a better expirience and a faster flow
1833 of work. Also the code should be more readable, more reliable and
1842 One input row, so that input happens every time at the same place.
1846 Use of pickers where possible.
1850 Possibility to enter more than one item at once.
1854 Item list in a scrollable area, so that the workflow buttons stay at
1859 Reordering item rows with drag and drop is possible. Sorting item rows is
1860 possible (by partnumber, description, qty, sellprice and discount for now).
1864 No C<update> is necessary. All entries and calculations are managed
1865 with ajax-calls and the page does only reload on C<save>.
1869 User can see changes immediately, because of the use of java script
1880 =item * C<SL/Controller/Order.pm>
1884 =item * C<template/webpages/order/form.html>
1888 =item * C<template/webpages/order/tabs/basic_data.html>
1890 Main tab for basic_data.
1892 This is the only tab here for now. "linked records" and "webdav" tabs are
1893 reused from generic code.
1897 =item * C<template/webpages/order/tabs/_business_info_row.html>
1899 For displaying information on business type
1901 =item * C<template/webpages/order/tabs/_item_input.html>
1903 The input line for items
1905 =item * C<template/webpages/order/tabs/_row.html>
1907 One row for already entered items
1909 =item * C<template/webpages/order/tabs/_tax_row.html>
1911 Displaying tax information
1913 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1915 Dialog for entering more than one item at once
1917 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1919 Results for the filter in the multi items dialog
1921 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1923 Dialog for selecting price and discount sources
1927 =item * C<js/kivi.Order.js>
1929 java script functions
1941 =item * credit limit
1943 =item * more workflows (quotation, rfq)
1945 =item * price sources: little symbols showing better price / better discount
1947 =item * select units in input row?
1949 =item * custom shipto address
1951 =item * check for direct delivery (workflow sales order -> purchase order)
1953 =item * language / part translations
1955 =item * access rights
1957 =item * display weights
1963 =item * optional client/user behaviour
1965 (transactions has to be set - department has to be set -
1966 force project if enabled in client config - transport cost reminder)
1970 =head1 KNOWN BUGS AND CAVEATS
1976 Customer discount is not displayed as a valid discount in price source popup
1977 (this might be a bug in price sources)
1979 (I cannot reproduce this (Bernd))
1983 No indication that <shift>-up/down expands/collapses second row.
1987 Inline creation of parts is not currently supported
1991 Table header is not sticky in the scrolling area.
1995 Sorting does not include C<position>, neither does reordering.
1997 This behavior was implemented intentionally. But we can discuss, which behavior
1998 should be implemented.
2002 C<show_multi_items_dialog> does not use the currently inserted string for
2007 The language selected in print or email dialog is not saved when the order is saved.
2011 =head1 To discuss / Nice to have
2017 How to expand/collapse second row. Now it can be done clicking the icon or
2022 Possibility to change longdescription in input row?
2026 Possibility to select PriceSources in input row?
2030 This controller uses a (changed) copy of the template for the PriceSource
2031 dialog. Maybe there could be used one code source.
2035 Rounding-differences between this controller (PriceTaxCalculator) and the old
2036 form. This is not only a problem here, but also in all parts using the PTC.
2037 There exists a ticket and a patch. This patch should be testet.
2041 An indicator, if the actual inputs are saved (like in an
2042 editor or on text processing application).
2046 A warning when leaving the page without saveing unchanged inputs.
2053 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>