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_type() ? $::instance_conf->get_reqdate_interval : 1;
62 $self->order->reqdate(DateTime->today_local->next_workday(extra_days => $extra_days)) if !$self->order->reqdate;
67 title => $self->get_title_for('add'),
68 %{$self->{template_args}}
72 # edit an existing order
80 # this is to edit an order from an unsaved order object
82 # set item ids to new fake id, to identify them as new items
83 foreach my $item (@{$self->order->items_sorted}) {
84 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
86 # trigger rendering values for second row/longdescription as hidden,
87 # because they are loaded only on demand. So we need to keep the values
89 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
90 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
97 title => $self->get_title_for('edit'),
98 %{$self->{template_args}}
102 # edit a collective order (consisting of one or more existing orders)
103 sub action_edit_collective {
107 my @multi_ids = map {
108 $_ =~ m{^multi_id_(\d+)$} && $::form->{'multi_id_' . $1} && $::form->{'trans_id_' . $1} && $::form->{'trans_id_' . $1}
109 } grep { $_ =~ m{^multi_id_\d+$} } keys %$::form;
111 # fall back to add if no ids are given
112 if (scalar @multi_ids == 0) {
117 # fall back to save as new if only one id is given
118 if (scalar @multi_ids == 1) {
119 $self->order(SL::DB::Order->new(id => $multi_ids[0])->load);
120 $self->action_save_as_new();
124 # make new order from given orders
125 my @multi_orders = map { SL::DB::Order->new(id => $_)->load } @multi_ids;
126 $self->{converted_from_oe_id} = join ' ', map { $_->id } @multi_orders;
127 $self->order(SL::DB::Order->new_from_multi(\@multi_orders, sort_sources_by => 'transdate'));
129 $self->action_edit();
136 my $errors = $self->delete();
138 if (scalar @{ $errors }) {
139 $self->js->flash('error', $_) foreach @{ $errors };
140 return $self->js->render();
143 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been deleted')
144 : $self->type eq purchase_order_type() ? $::locale->text('The order has been deleted')
145 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been deleted')
146 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been deleted')
148 flash_later('info', $text);
150 my @redirect_params = (
155 $self->redirect_to(@redirect_params);
162 my $errors = $self->save();
164 if (scalar @{ $errors }) {
165 $self->js->flash('error', $_) foreach @{ $errors };
166 return $self->js->render();
169 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
170 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
171 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
172 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
174 flash_later('info', $text);
176 my @redirect_params = (
179 id => $self->order->id,
182 $self->redirect_to(@redirect_params);
185 # save the order as new document an open it for edit
186 sub action_save_as_new {
189 my $order = $self->order;
192 $self->js->flash('error', t8('This object has not been saved yet.'));
193 return $self->js->render();
196 # load order from db to check if values changed
197 my $saved_order = SL::DB::Order->new(id => $order->id)->load;
200 # Lets assign a new number if the user hasn't changed the previous one.
201 # If it has been changed manually then use it as-is.
202 $new_attrs{number} = (trim($order->number) eq $saved_order->number)
204 : trim($order->number);
206 # Clear transdate unless changed
207 $new_attrs{transdate} = ($order->transdate == $saved_order->transdate)
208 ? DateTime->today_local
211 # Set new reqdate unless changed
212 if ($order->reqdate == $saved_order->reqdate) {
213 my $extra_days = $self->type eq sales_quotation_type() ? $::instance_conf->get_reqdate_interval : 1;
214 $new_attrs{reqdate} = DateTime->today_local->next_workday(extra_days => $extra_days);
216 $new_attrs{reqdate} = $order->reqdate;
220 $new_attrs{employee} = SL::DB::Manager::Employee->current;
222 # Create new record from current one
223 $self->order(SL::DB::Order->new_from($order, destination_type => $order->type, attributes => \%new_attrs));
225 # no linked records on save as new
226 delete $::form->{$_} for qw(converted_from_oe_id converted_from_orderitems_ids);
229 $self->action_save();
234 # This is called if "print" is pressed in the print dialog.
235 # If PDF creation was requested and succeeded, the pdf is stored in a session
236 # file and the filename is stored as session value with an unique key. A
237 # javascript function with this key is then called. This function calls the
238 # download action below (action_download_pdf), which offers the file for
243 my $errors = $self->save();
245 if (scalar @{ $errors }) {
246 $self->js->flash('error', $_) foreach @{ $errors };
247 return $self->js->render();
250 $self->js->val('#id', $self->order->id)
251 ->val('#order_' . $self->nr_key(), $self->order->number);
253 my $format = $::form->{print_options}->{format};
254 my $media = $::form->{print_options}->{media};
255 my $formname = $::form->{print_options}->{formname};
256 my $copies = $::form->{print_options}->{copies};
257 my $groupitems = $::form->{print_options}->{groupitems};
259 # only pdf and opendocument by now
260 if (none { $format eq $_ } qw(pdf opendocument opendocument_pdf)) {
261 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
264 # only screen or printer by now
265 if (none { $media eq $_ } qw(screen printer)) {
266 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
270 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
272 # create a form for generate_attachment_filename
273 my $form = Form->new;
274 $form->{$self->nr_key()} = $self->order->number;
275 $form->{type} = $self->type;
276 $form->{format} = $format;
277 $form->{formname} = $formname;
278 $form->{language} = '_' . $language->template_code if $language;
279 my $pdf_filename = $form->generate_attachment_filename();
282 my @errors = generate_pdf($self->order, \$pdf, { format => $format,
283 formname => $formname,
284 language => $language,
285 groupitems => $groupitems });
286 if (scalar @errors) {
287 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
290 if ($media eq 'screen') {
292 my $sfile = SL::SessionFile::Random->new(mode => "w");
293 $sfile->fh->print($pdf);
296 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
297 $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
300 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
301 ->flash('info', t8('The PDF has been created'));
303 } elsif ($media eq 'printer') {
305 my $printer_id = $::form->{print_options}->{printer_id};
306 SL::DB::Printer->new(id => $printer_id)->load->print_document(
311 $self->js->flash('info', t8('The PDF has been printed'));
314 # copy file to webdav folder
315 if ($self->order->number && $::instance_conf->get_webdav_documents) {
316 my $webdav = SL::Webdav->new(
318 number => $self->order->number,
320 my $webdav_file = SL::Webdav::File->new(
322 filename => $pdf_filename,
325 $webdav_file->store(data => \$pdf);
328 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
331 if ($self->order->number && $::instance_conf->get_doc_storage) {
333 SL::File->save(object_id => $self->order->id,
334 object_type => $self->type,
335 mime_type => 'application/pdf',
337 file_type => 'document',
338 file_name => $pdf_filename,
339 file_contents => $pdf);
342 $self->js->flash('error', t8('Storing PDF in storage backend failed: #1', $@));
348 # offer pdf for download
350 # It needs to get the key for the session value to get the pdf file.
351 sub action_download_pdf {
354 my $key = $::form->{key};
355 my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
356 return $self->send_file(
358 type => 'application/pdf',
359 name => $::form->{pdf_filename},
363 # open the email dialog
364 sub action_show_email_dialog {
367 my $cv_method = $self->cv;
369 if (!$self->order->$cv_method) {
370 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'))
375 $email_form->{to} = $self->order->contact->cp_email if $self->order->contact;
376 $email_form->{to} ||= $self->order->$cv_method->email;
377 $email_form->{cc} = $self->order->$cv_method->cc;
378 $email_form->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
379 # Todo: get addresses from shipto, if any
381 my $form = Form->new;
382 $form->{$self->nr_key()} = $self->order->number;
383 $form->{formname} = $self->type;
384 $form->{type} = $self->type;
385 $form->{language} = 'de';
386 $form->{format} = 'pdf';
388 $email_form->{subject} = $form->generate_email_subject();
389 $email_form->{attachment_filename} = $form->generate_attachment_filename();
390 $email_form->{message} = $form->generate_email_body();
391 $email_form->{js_send_function} = 'kivi.Order.send_email()';
393 my %files = $self->get_files_for_email_dialog();
394 my $dialog_html = $self->render('common/_send_email_dialog', { output => 0 },
395 email_form => $email_form,
396 show_bcc => $::auth->assert('email_bcc', 'may fail'),
398 is_customer => $self->cv eq 'customer',
402 ->run('kivi.Order.show_email_dialog', $dialog_html)
409 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
410 sub action_send_email {
413 my $errors = $self->save();
415 if (scalar @{ $errors }) {
416 $self->js->run('kivi.Order.close_email_dialog');
417 $self->js->flash('error', $_) foreach @{ $errors };
418 return $self->js->render();
421 $self->js->val('#id', $self->order->id)
422 ->val('#order_' . $self->nr_key(), $self->order->number);
424 my $email_form = delete $::form->{email_form};
425 my %field_names = (to => 'email');
427 $::form->{ $field_names{$_} // $_ } = $email_form->{$_} for keys %{ $email_form };
429 # for Form::cleanup which may be called in Form::send_email
430 $::form->{cwd} = getcwd();
431 $::form->{tmpdir} = $::lx_office_conf{paths}->{userspath};
433 $::form->{$_} = $::form->{print_options}->{$_} for keys %{ $::form->{print_options} };
434 $::form->{media} = 'email';
436 if (($::form->{attachment_policy} // '') !~ m{^(?:old_file|no_file)$}) {
438 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
441 my @errors = generate_pdf($self->order, \$pdf, {media => $::form->{media},
442 format => $::form->{print_options}->{format},
443 formname => $::form->{print_options}->{formname},
444 language => $language,
445 groupitems => $::form->{print_options}->{groupitems}});
446 if (scalar @errors) {
447 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
450 my $sfile = SL::SessionFile::Random->new(mode => "w");
451 $sfile->fh->print($pdf);
454 $::form->{tmpfile} = $sfile->file_name;
455 $::form->{tmpdir} = $sfile->get_path; # for Form::cleanup which may be called in Form::send_email
458 $::form->send_email(\%::myconfig, 'pdf');
461 my $intnotes = $self->order->intnotes;
462 $intnotes .= "\n\n" if $self->order->intnotes;
463 $intnotes .= t8('[email]') . "\n";
464 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
465 $intnotes .= t8('To (email)') . ": " . $::form->{email} . "\n";
466 $intnotes .= t8('Cc') . ": " . $::form->{cc} . "\n" if $::form->{cc};
467 $intnotes .= t8('Bcc') . ": " . $::form->{bcc} . "\n" if $::form->{bcc};
468 $intnotes .= t8('Subject') . ": " . $::form->{subject} . "\n\n";
469 $intnotes .= t8('Message') . ": " . $::form->{message};
471 $self->order->update_attributes(intnotes => $intnotes);
474 ->val('#order_intnotes', $intnotes)
475 ->run('kivi.Order.close_email_dialog')
476 ->flash('info', t8('The email has been sent.'))
480 # open the periodic invoices config dialog
482 # If there are values in the form (i.e. dialog was opened before),
483 # then use this values. Create new ones, else.
484 sub action_show_periodic_invoices_config_dialog {
487 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
488 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
489 $config ||= SL::DB::PeriodicInvoicesConfig->new(periodicity => 'm',
490 order_value_periodicity => 'p', # = same as periodicity
491 start_date_as_date => $::form->{transdate_as_date} || $::form->current_date,
492 extend_automatically_by => 12,
494 email_subject => GenericTranslations->get(
495 language_id => $::form->{language_id},
496 translation_type =>"preset_text_periodic_invoices_email_subject"),
497 email_body => GenericTranslations->get(
498 language_id => $::form->{language_id},
499 translation_type =>"preset_text_periodic_invoices_email_body"),
501 $config->periodicity('m') if none { $_ eq $config->periodicity } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
502 $config->order_value_periodicity('p') if none { $_ eq $config->order_value_periodicity } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
504 $::form->get_lists(printers => "ALL_PRINTERS",
505 charts => { key => 'ALL_CHARTS',
506 transdate => 'current_date' });
508 $::form->{AR} = [ grep { $_->{link} =~ m/(?:^|:)AR(?::|$)/ } @{ $::form->{ALL_CHARTS} } ];
510 if ($::form->{customer_id}) {
511 $::form->{ALL_CONTACTS} = SL::DB::Manager::Contact->get_all_sorted(where => [ cp_cv_id => $::form->{customer_id} ]);
512 $::form->{email_recipient_invoice_address} = SL::DB::Manager::Customer->find_by(id => $::form->{customer_id})->invoice_mail;
515 $self->render('oe/edit_periodic_invoices_config', { layout => 0 },
517 popup_js_close_function => 'kivi.Order.close_periodic_invoices_config_dialog()',
518 popup_js_assign_function => 'kivi.Order.assign_periodic_invoices_config()',
523 # assign the values of the periodic invoices config dialog
524 # as yaml in the hidden tag and set the status.
525 sub action_assign_periodic_invoices_config {
528 $::form->isblank('start_date_as_date', $::locale->text('The start date is missing.'));
530 my $config = { active => $::form->{active} ? 1 : 0,
531 terminated => $::form->{terminated} ? 1 : 0,
532 direct_debit => $::form->{direct_debit} ? 1 : 0,
533 periodicity => (any { $_ eq $::form->{periodicity} } @SL::DB::PeriodicInvoicesConfig::PERIODICITIES) ? $::form->{periodicity} : 'm',
534 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
535 start_date_as_date => $::form->{start_date_as_date},
536 end_date_as_date => $::form->{end_date_as_date},
537 first_billing_date_as_date => $::form->{first_billing_date_as_date},
538 print => $::form->{print} ? 1 : 0,
539 printer_id => $::form->{print} ? $::form->{printer_id} * 1 : undef,
540 copies => $::form->{copies} * 1 ? $::form->{copies} : 1,
541 extend_automatically_by => $::form->{extend_automatically_by} * 1 || undef,
542 ar_chart_id => $::form->{ar_chart_id} * 1,
543 send_email => $::form->{send_email} ? 1 : 0,
544 email_recipient_contact_id => $::form->{email_recipient_contact_id} * 1 || undef,
545 email_recipient_address => $::form->{email_recipient_address},
546 email_sender => $::form->{email_sender},
547 email_subject => $::form->{email_subject},
548 email_body => $::form->{email_body},
551 my $periodic_invoices_config = YAML::Dump($config);
553 my $status = $self->get_periodic_invoices_status($config);
556 ->remove('#order_periodic_invoices_config')
557 ->insertAfter(hidden_tag('order.periodic_invoices_config', $periodic_invoices_config), '#periodic_invoices_status')
558 ->run('kivi.Order.close_periodic_invoices_config_dialog')
559 ->html('#periodic_invoices_status', $status)
560 ->flash('info', t8('The periodic invoices config has been assigned.'))
564 sub action_get_has_active_periodic_invoices {
567 my $config = make_periodic_invoices_config_from_yaml(delete $::form->{config});
568 $config ||= SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $::form->{id}) if $::form->{id};
570 my $has_active_periodic_invoices =
571 $self->type eq sales_order_type()
574 && (!$config->end_date || ($config->end_date > DateTime->today_local))
575 && $config->get_previous_billed_period_start_date;
577 $_[0]->render(\ !!$has_active_periodic_invoices, { type => 'text' });
580 # save the order and redirect to the frontend subroutine for a new
582 sub action_save_and_delivery_order {
585 my $errors = $self->save();
587 if (scalar @{ $errors }) {
588 $self->js->flash('error', $_) foreach @{ $errors };
589 return $self->js->render();
592 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
593 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
594 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
595 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
597 flash_later('info', $text);
599 my @redirect_params = (
600 controller => 'oe.pl',
601 action => 'oe_delivery_order_from_order',
602 id => $self->order->id,
605 $self->redirect_to(@redirect_params);
608 # save the order and redirect to the frontend subroutine for a new
610 sub action_save_and_invoice {
613 my $errors = $self->save();
615 if (scalar @{ $errors }) {
616 $self->js->flash('error', $_) foreach @{ $errors };
617 return $self->js->render();
620 my $text = $self->type eq sales_order_type() ? $::locale->text('The order has been saved')
621 : $self->type eq purchase_order_type() ? $::locale->text('The order has been saved')
622 : $self->type eq sales_quotation_type() ? $::locale->text('The quotation has been saved')
623 : $self->type eq request_quotation_type() ? $::locale->text('The rfq has been saved')
625 flash_later('info', $text);
627 my @redirect_params = (
628 controller => 'oe.pl',
629 action => 'oe_invoice_from_order',
630 id => $self->order->id,
633 $self->redirect_to(@redirect_params);
636 # workflow from sales quotation to sales order
637 sub action_sales_order {
638 $_[0]->workflow_sales_or_purchase_order();
641 # workflow from rfq to purchase order
642 sub action_purchase_order {
643 $_[0]->workflow_sales_or_purchase_order();
646 # set form elements in respect to a changed customer or vendor
648 # This action is called on an change of the customer/vendor picker.
649 sub action_customer_vendor_changed {
652 setup_order_from_cv($self->order);
655 my $cv_method = $self->cv;
657 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
658 $self->js->show('#cp_row');
660 $self->js->hide('#cp_row');
663 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
664 $self->js->show('#shipto_row');
666 $self->js->hide('#shipto_row');
669 $self->js->val( '#order_salesman_id', $self->order->salesman_id) if $self->order->is_sales;
672 ->replaceWith('#order_cp_id', $self->build_contact_select)
673 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
674 ->replaceWith('#business_info_row', $self->build_business_info_row)
675 ->val( '#order_taxzone_id', $self->order->taxzone_id)
676 ->val( '#order_taxincluded', $self->order->taxincluded)
677 ->val( '#order_payment_id', $self->order->payment_id)
678 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
679 ->val( '#order_intnotes', $self->order->intnotes)
680 ->val( '#language_id', $self->order->$cv_method->language_id)
681 ->focus( '#order_' . $self->cv . '_id');
683 $self->js_redisplay_amounts_and_taxes;
687 # open the dialog for customer/vendor details
688 sub action_show_customer_vendor_details_dialog {
691 my $is_customer = 'customer' eq $::form->{vc};
694 $cv = SL::DB::Customer->new(id => $::form->{vc_id})->load;
696 $cv = SL::DB::Vendor->new(id => $::form->{vc_id})->load;
699 my %details = map { $_ => $cv->$_ } @{$cv->meta->columns};
700 $details{discount_as_percent} = $cv->discount_as_percent;
701 $details{creditlimt} = $cv->creditlimit_as_number;
702 $details{business} = $cv->business->description if $cv->business;
703 $details{language} = $cv->language_obj->description if $cv->language_obj;
704 $details{delivery_terms} = $cv->delivery_term->description if $cv->delivery_term;
705 $details{payment_terms} = $cv->payment->description if $cv->payment;
706 $details{pricegroup} = $cv->pricegroup->pricegroup if $is_customer && $cv->pricegroup;
708 foreach my $entry (@{ $cv->shipto }) {
709 push @{ $details{SHIPTO} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
711 foreach my $entry (@{ $cv->contacts }) {
712 push @{ $details{CONTACTS} }, { map { $_ => $entry->$_ } @{$entry->meta->columns} };
715 $_[0]->render('common/show_vc_details', { layout => 0 },
716 is_customer => $is_customer,
721 # called if a unit in an existing item row is changed
722 sub action_unit_changed {
725 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
726 my $item = $self->order->items_sorted->[$idx];
728 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
729 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
734 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
735 $self->js_redisplay_line_values;
736 $self->js_redisplay_amounts_and_taxes;
740 # add an item row for a new item entered in the input row
741 sub action_add_item {
744 my $form_attr = $::form->{add_item};
746 return unless $form_attr->{parts_id};
748 my $item = new_item($self->order, $form_attr);
750 $self->order->add_items($item);
754 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
755 my $row_as_html = $self->p->render('order/tabs/_row',
759 ALL_PRICE_FACTORS => $self->all_price_factors
763 ->append('#row_table_id', $row_as_html);
765 if ( $item->part->is_assortment ) {
766 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
767 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
768 my $attr = { parts_id => $assortment_item->parts_id,
769 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
770 unit => $assortment_item->unit,
771 description => $assortment_item->part->description,
773 my $item = new_item($self->order, $attr);
775 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
776 $item->discount(1) unless $assortment_item->charge;
778 $self->order->add_items( $item );
780 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
781 my $row_as_html = $self->p->render('order/tabs/_row',
785 ALL_PRICE_FACTORS => $self->all_price_factors
788 ->append('#row_table_id', $row_as_html);
793 ->val('.add_item_input', '')
794 ->run('kivi.Order.init_row_handlers')
795 ->run('kivi.Order.row_table_scroll_down')
796 ->run('kivi.Order.renumber_positions')
797 ->focus('#add_item_parts_id_name');
799 $self->js_redisplay_amounts_and_taxes;
803 # open the dialog for entering multiple items at once
804 sub action_show_multi_items_dialog {
805 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
806 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
809 # update the filter results in the multi item dialog
810 sub action_multi_items_update_result {
813 $::form->{multi_items}->{filter}->{obsolete} = 0;
815 my $count = $_[0]->multi_items_models->count;
818 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
819 $_[0]->render($text, { layout => 0 });
820 } elsif ($count > $max_count) {
821 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
822 $_[0]->render($text, { layout => 0 });
824 my $multi_items = $_[0]->multi_items_models->get;
825 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
826 multi_items => $multi_items);
830 # add item rows for multiple items at once
831 sub action_add_multi_items {
834 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
835 return $self->js->render() unless scalar @form_attr;
838 foreach my $attr (@form_attr) {
839 my $item = new_item($self->order, $attr);
841 if ( $item->part->is_assortment ) {
842 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
843 my $attr = { parts_id => $assortment_item->parts_id,
844 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
845 unit => $assortment_item->unit,
846 description => $assortment_item->part->description,
848 my $item = new_item($self->order, $attr);
850 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
851 $item->discount(1) unless $assortment_item->charge;
856 $self->order->add_items(@items);
860 foreach my $item (@items) {
861 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
862 my $row_as_html = $self->p->render('order/tabs/_row',
866 ALL_PRICE_FACTORS => $self->all_price_factors
869 $self->js->append('#row_table_id', $row_as_html);
873 ->run('kivi.Order.close_multi_items_dialog')
874 ->run('kivi.Order.init_row_handlers')
875 ->run('kivi.Order.row_table_scroll_down')
876 ->run('kivi.Order.renumber_positions')
877 ->focus('#add_item_parts_id_name');
879 $self->js_redisplay_amounts_and_taxes;
883 # recalculate all linetotals, amounts and taxes and redisplay them
884 sub action_recalc_amounts_and_taxes {
889 $self->js_redisplay_line_values;
890 $self->js_redisplay_amounts_and_taxes;
894 # redisplay item rows if they are sorted by an attribute
895 sub action_reorder_items {
899 partnumber => sub { $_[0]->part->partnumber },
900 description => sub { $_[0]->description },
901 qty => sub { $_[0]->qty },
902 sellprice => sub { $_[0]->sellprice },
903 discount => sub { $_[0]->discount },
906 my $method = $sort_keys{$::form->{order_by}};
907 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
908 if ($::form->{sort_dir}) {
909 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
911 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
914 ->run('kivi.Order.redisplay_items', \@to_sort)
918 # show the popup to choose a price/discount source
919 sub action_price_popup {
922 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
923 my $item = $self->order->items_sorted->[$idx];
925 $self->render_price_dialog($item);
928 # get the longdescription for an item if the dialog to enter/change the
929 # longdescription was opened and the longdescription is empty
931 # If this item is new, get the longdescription from Part.
932 # Otherwise get it from OrderItem.
933 sub action_get_item_longdescription {
936 if ($::form->{item_id}) {
937 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
938 } elsif ($::form->{parts_id}) {
939 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
941 $_[0]->render(\ $longdescription, { type => 'text' });
944 # load the second row for one or more items
946 # This action gets the html code for all items second rows by rendering a template for
947 # the second row and sets the html code via client js.
948 sub action_load_second_rows {
951 $self->recalc() if $self->order->is_sales; # for margin calculation
953 foreach my $item_id (@{ $::form->{item_ids} }) {
954 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
955 my $item = $self->order->items_sorted->[$idx];
957 $self->js_load_second_row($item, $item_id, 0);
960 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
965 sub js_load_second_row {
966 my ($self, $item, $item_id, $do_parse) = @_;
969 # Parse values from form (they are formated while rendering (template)).
970 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
971 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
972 foreach my $var (@{ $item->cvars_by_config }) {
973 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
975 $item->parse_custom_variable_values;
978 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item, TYPE => $self->type);
981 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
982 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
985 sub js_redisplay_line_values {
988 my $is_sales = $self->order->is_sales;
990 # sales orders with margins
995 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
996 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
997 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
998 ]} @{ $self->order->items_sorted };
1002 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
1003 ]} @{ $self->order->items_sorted };
1007 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
1010 sub js_redisplay_amounts_and_taxes {
1013 if (scalar @{ $self->{taxes} }) {
1014 $self->js->show('#taxincluded_row_id');
1016 $self->js->hide('#taxincluded_row_id');
1019 if ($self->order->taxincluded) {
1020 $self->js->hide('#subtotal_row_id');
1022 $self->js->show('#subtotal_row_id');
1025 if ($self->order->is_sales) {
1026 my $is_neg = $self->order->marge_total < 0;
1028 ->html('#marge_total_id', $::form->format_amount(\%::myconfig, $self->order->marge_total, 2))
1029 ->html('#marge_percent_id', $::form->format_amount(\%::myconfig, $self->order->marge_percent, 2))
1030 ->action_if( $is_neg, 'addClass', '#marge_total_id', 'plus0')
1031 ->action_if( $is_neg, 'addClass', '#marge_percent_id', 'plus0')
1032 ->action_if( $is_neg, 'addClass', '#marge_percent_sign_id', 'plus0')
1033 ->action_if(!$is_neg, 'removeClass', '#marge_total_id', 'plus0')
1034 ->action_if(!$is_neg, 'removeClass', '#marge_percent_id', 'plus0')
1035 ->action_if(!$is_neg, 'removeClass', '#marge_percent_sign_id', 'plus0');
1039 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
1040 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
1041 ->remove('.tax_row')
1042 ->insertBefore($self->build_tax_rows, '#amount_row_id');
1049 sub init_valid_types {
1050 [ sales_order_type(), purchase_order_type(), sales_quotation_type(), request_quotation_type() ];
1056 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
1057 die "Not a valid type for order";
1060 $self->type($::form->{type});
1066 my $cv = (any { $self->type eq $_ } (sales_order_type(), sales_quotation_type())) ? 'customer'
1067 : (any { $self->type eq $_ } (purchase_order_type(), request_quotation_type())) ? 'vendor'
1068 : die "Not a valid type for order";
1081 # model used to filter/display the parts in the multi-items dialog
1082 sub init_multi_items_models {
1083 SL::Controller::Helper::GetModels->new(
1084 controller => $_[0],
1086 with_objects => [ qw(unit_obj) ],
1087 disable_plugin => 'paginated',
1088 source => $::form->{multi_items},
1094 partnumber => t8('Partnumber'),
1095 description => t8('Description')}
1099 sub init_all_price_factors {
1100 SL::DB::Manager::PriceFactor->get_all;
1106 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
1108 my $right = $right_for->{ $self->type };
1109 $right ||= 'DOES_NOT_EXIST';
1111 $::auth->assert($right);
1114 # build the selection box for contacts
1116 # Needed, if customer/vendor changed.
1117 sub build_contact_select {
1120 select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
1121 value_key => 'cp_id',
1122 title_key => 'full_name_dep',
1123 default => $self->order->cp_id,
1125 style => 'width: 300px',
1129 # build the selection box for shiptos
1131 # Needed, if customer/vendor changed.
1132 sub build_shipto_select {
1135 select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
1136 value_key => 'shipto_id',
1137 title_key => 'displayable_id',
1138 default => $self->order->shipto_id,
1140 style => 'width: 300px',
1144 # render the info line for business
1146 # Needed, if customer/vendor changed.
1147 sub build_business_info_row
1149 $_[0]->p->render('order/tabs/_business_info_row', SELF => $_[0]);
1152 # build the rows for displaying taxes
1154 # Called if amounts where recalculated and redisplayed.
1155 sub build_tax_rows {
1159 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
1160 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
1162 return $rows_as_html;
1166 sub render_price_dialog {
1167 my ($self, $record_item) = @_;
1169 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
1173 'kivi.io.price_chooser_dialog',
1174 t8('Available Prices'),
1175 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
1180 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
1181 # $self->js->show('#dialog_flash_error');
1190 return if !$::form->{id};
1192 $self->order(SL::DB::Order->new(id => $::form->{id})->load);
1195 # load or create a new order object
1197 # And assign changes from the form to this object.
1198 # If the order is loaded from db, check if items are deleted in the form,
1199 # remove them form the object and collect them for removing from db on saving.
1200 # Then create/update items from form (via make_item) and add them.
1204 # add_items adds items to an order with no items for saving, but they cannot
1205 # be retrieved via items until the order is saved. Adding empty items to new
1206 # order here solves this problem.
1208 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
1209 $order ||= SL::DB::Order->new(orderitems => [],
1210 quotation => (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type())));
1212 my $cv_id_method = $self->cv . '_id';
1213 if (!$::form->{id} && $::form->{$cv_id_method}) {
1214 $order->$cv_id_method($::form->{$cv_id_method});
1215 setup_order_from_cv($order);
1218 my $form_orderitems = delete $::form->{order}->{orderitems};
1219 my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
1221 $order->assign_attributes(%{$::form->{order}});
1223 if (my $periodic_invoices_config_attrs = $form_periodic_invoices_config ? YAML::Load($form_periodic_invoices_config) : undef) {
1224 my $periodic_invoices_config = $order->periodic_invoices_config || $order->periodic_invoices_config(SL::DB::PeriodicInvoicesConfig->new);
1225 $periodic_invoices_config->assign_attributes(%$periodic_invoices_config_attrs);
1228 # remove deleted items
1229 $self->item_ids_to_delete([]);
1230 foreach my $idx (reverse 0..$#{$order->orderitems}) {
1231 my $item = $order->orderitems->[$idx];
1232 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
1233 splice @{$order->orderitems}, $idx, 1;
1234 push @{$self->item_ids_to_delete}, $item->id;
1240 foreach my $form_attr (@{$form_orderitems}) {
1241 my $item = make_item($order, $form_attr);
1242 $item->position($pos);
1246 $order->add_items(grep {!$_->id} @items);
1251 # create or update items from form
1253 # Make item objects from form values. For items already existing read from db.
1254 # Create a new item else. And assign attributes.
1256 my ($record, $attr) = @_;
1259 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
1261 my $is_new = !$item;
1263 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1264 # they cannot be retrieved via custom_variables until the order/orderitem is
1265 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1266 $item ||= SL::DB::OrderItem->new(custom_variables => []);
1268 $item->assign_attributes(%$attr);
1269 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
1270 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
1271 $item->lastcost($record->is_sales ? $item->part->lastcost : 0) if $is_new && !defined $attr->{lastcost_as_number};
1278 # This is used to add one item
1280 my ($record, $attr) = @_;
1282 my $item = SL::DB::OrderItem->new;
1284 # Remove attributes where the user left or set the inputs empty.
1285 # So these attributes will be undefined and we can distinguish them
1286 # from zero later on.
1287 for (qw(qty_as_number sellprice_as_number discount_as_percent)) {
1288 delete $attr->{$_} if $attr->{$_} eq '';
1291 $item->assign_attributes(%$attr);
1293 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
1294 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
1296 $item->unit($part->unit) if !$item->unit;
1299 if ( $part->is_assortment ) {
1300 # add assortment items with price 0, as the components carry the price
1301 $price_src = $price_source->price_from_source("");
1302 $price_src->price(0);
1303 } elsif (defined $item->sellprice) {
1304 $price_src = $price_source->price_from_source("");
1305 $price_src->price($item->sellprice);
1307 $price_src = $price_source->best_price
1308 ? $price_source->best_price
1309 : $price_source->price_from_source("");
1310 $price_src->price(0) if !$price_source->best_price;
1314 if (defined $item->discount) {
1315 $discount_src = $price_source->discount_from_source("");
1316 $discount_src->discount($item->discount);
1318 $discount_src = $price_source->best_discount
1319 ? $price_source->best_discount
1320 : $price_source->discount_from_source("");
1321 $discount_src->discount(0) if !$price_source->best_discount;
1325 $new_attr{part} = $part;
1326 $new_attr{description} = $part->description if ! $item->description;
1327 $new_attr{qty} = 1.0 if ! $item->qty;
1328 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
1329 $new_attr{sellprice} = $price_src->price;
1330 $new_attr{discount} = $discount_src->discount;
1331 $new_attr{active_price_source} = $price_src;
1332 $new_attr{active_discount_source} = $discount_src;
1333 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
1334 $new_attr{project_id} = $record->globalproject_id;
1335 $new_attr{lastcost} = $record->is_sales ? $part->lastcost : 0;
1337 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
1338 # they cannot be retrieved via custom_variables until the order/orderitem is
1339 # saved. Adding empty custom_variables to new orderitem here solves this problem.
1340 $new_attr{custom_variables} = [];
1342 $item->assign_attributes(%new_attr);
1347 sub setup_order_from_cv {
1350 $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
1352 $order->intnotes($order->customervendor->notes);
1354 if ($order->is_sales) {
1355 $order->salesman_id($order->customer->salesman_id || SL::DB::Manager::Employee->current->id);
1356 $order->taxincluded(defined($order->customer->taxincluded_checked)
1357 ? $order->customer->taxincluded_checked
1358 : $::myconfig{taxincluded_checked});
1363 # recalculate prices and taxes
1365 # Using the PriceTaxCalculator. Store linetotals in the item objects.
1369 # bb: todo: currency later
1370 $self->order->currency_id($::instance_conf->get_currency_id());
1372 my %pat = $self->order->calculate_prices_and_taxes();
1373 $self->{taxes} = [];
1374 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
1375 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
1377 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
1378 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
1379 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
1383 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
1386 # get data for saving, printing, ..., that is not changed in the form
1388 # Only cvars for now.
1389 sub get_unalterable_data {
1392 foreach my $item (@{ $self->order->items }) {
1393 # autovivify all cvars that are not in the form (cvars_by_config can do it).
1394 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
1395 foreach my $var (@{ $item->cvars_by_config }) {
1396 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
1398 $item->parse_custom_variable_values;
1404 # And remove related files in the spool directory
1409 my $db = $self->order->db;
1411 $db->with_transaction(
1413 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1414 $self->order->delete;
1415 my $spool = $::lx_office_conf{paths}->{spool};
1416 unlink map { "$spool/$_" } @spoolfiles if $spool;
1419 }) || push(@{$errors}, $db->error);
1426 # And delete items that are deleted in the form.
1431 my $db = $self->order->db;
1433 $db->with_transaction(sub {
1434 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
1435 $self->order->save(cascade => 1);
1438 if ($::form->{converted_from_oe_id}) {
1439 my @converted_from_oe_ids = split ' ', $::form->{converted_from_oe_id};
1440 foreach my $converted_from_oe_id (@converted_from_oe_ids) {
1441 my $src = SL::DB::Order->new(id => $converted_from_oe_id)->load;
1442 $src->update_attributes(closed => 1) if $src->type =~ /_quotation$/;
1443 $src->link_to_record($self->order);
1445 if (scalar @{ $::form->{converted_from_orderitems_ids} || [] }) {
1447 foreach (@{ $self->order->items_sorted }) {
1448 my $from_id = $::form->{converted_from_orderitems_ids}->[$idx];
1450 SL::DB::RecordLink->new(from_table => 'orderitems',
1451 from_id => $from_id,
1452 to_table => 'orderitems',
1460 }) || push(@{$errors}, $db->error);
1465 sub workflow_sales_or_purchase_order {
1468 my $destination_type = $::form->{type} eq sales_quotation_type() ? sales_order_type()
1469 : $::form->{type} eq request_quotation_type() ? purchase_order_type()
1470 : $::form->{type} eq purchase_order_type() ? sales_order_type()
1471 : $::form->{type} eq sales_order_type() ? purchase_order_type()
1474 $self->order(SL::DB::Order->new_from($self->order, destination_type => $destination_type));
1475 $self->{converted_from_oe_id} = delete $::form->{id};
1477 # set item ids to new fake id, to identify them as new items
1478 foreach my $item (@{$self->order->items_sorted}) {
1479 $item->{new_fake_id} = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
1483 $::form->{type} = $destination_type;
1484 $self->type($self->init_type);
1485 $self->cv ($self->init_cv);
1489 $self->get_unalterable_data();
1490 $self->pre_render();
1492 # trigger rendering values for second row/longdescription as hidden,
1493 # because they are loaded only on demand. So we need to keep the values
1495 $_->{render_second_row} = 1 for @{ $self->order->items_sorted };
1496 $_->{render_longdescription} = 1 for @{ $self->order->items_sorted };
1500 title => $self->get_title_for('edit'),
1501 %{$self->{template_args}}
1509 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1510 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1511 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1514 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1517 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1519 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1520 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1521 $self->{periodic_invoices_status} = $self->get_periodic_invoices_status($self->order->periodic_invoices_config);
1522 $self->{order_probabilities} = [ map { { title => ($_ * 10) . '%', id => $_ * 10 } } (0..10) ];
1524 my $print_form = Form->new('');
1525 $print_form->{type} = $self->type;
1526 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1527 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1528 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1529 form => $print_form,
1530 options => {dialog_name_prefix => 'print_options.',
1534 no_opendocument => 0,
1538 foreach my $item (@{$self->order->orderitems}) {
1539 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1540 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1541 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1544 if (any { $self->type eq $_ } (sales_order_type(), purchase_order_type())) {
1545 # calculate shipped qtys here to prevent calling calculate for every item via the items method
1546 SL::Helper::ShippedQty->new->calculate($self->order)->write_to_objects;
1549 if ($self->order->number && $::instance_conf->get_webdav) {
1550 my $webdav = SL::Webdav->new(
1551 type => $self->type,
1552 number => $self->order->number,
1554 my @all_objects = $webdav->get_all_objects;
1555 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1557 link => File::Spec->catfile($_->full_filedescriptor),
1561 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
1562 $self->setup_edit_action_bar;
1565 sub setup_edit_action_bar {
1566 my ($self, %params) = @_;
1568 my $deletion_allowed = (any { $self->type eq $_ } (sales_quotation_type(), request_quotation_type()))
1569 || (($self->type eq sales_order_type()) && $::instance_conf->get_sales_order_show_delete)
1570 || (($self->type eq purchase_order_type()) && $::instance_conf->get_purchase_order_show_delete);
1572 for my $bar ($::request->layout->get('actionbar')) {
1577 call => [ 'kivi.Order.save', 'save', $::instance_conf->get_order_warn_duplicate_parts,
1578 $::instance_conf->get_order_warn_no_deliverydate,
1580 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1584 call => [ 'kivi.Order.save', 'save_as_new', $::instance_conf->get_order_warn_duplicate_parts ],
1585 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1586 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1588 ], # end of combobox "Save"
1596 submit => [ '#order_form', { action => "Order/sales_order" } ],
1597 only_if => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
1598 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1601 t8('Purchase Order'),
1602 submit => [ '#order_form', { action => "Order/purchase_order" } ],
1603 only_if => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
1604 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1607 t8('Save and Delivery Order'),
1608 call => [ 'kivi.Order.save', 'save_and_delivery_order', $::instance_conf->get_order_warn_duplicate_parts,
1609 $::instance_conf->get_order_warn_no_deliverydate,
1611 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1612 only_if => (any { $self->type eq $_ } (sales_order_type(), purchase_order_type()))
1615 t8('Save and Invoice'),
1616 call => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
1617 checks => [ 'kivi.Order.check_save_active_periodic_invoices' ],
1619 ], # end of combobox "Workflow"
1626 t8('Save and print'),
1627 call => [ 'kivi.Order.show_print_options', $::instance_conf->get_order_warn_duplicate_parts ],
1630 t8('Save and E-mail'),
1631 call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
1634 t8('Download attachments of all parts'),
1635 call => [ 'kivi.File.downloadOrderitemsFiles', $::form->{type}, $::form->{id} ],
1636 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1637 only_if => $::instance_conf->get_doc_storage,
1639 ], # end of combobox "Export"
1643 call => [ 'kivi.Order.delete_order' ],
1644 confirm => $::locale->text('Do you really want to delete this object?'),
1645 disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
1646 only_if => $deletion_allowed,
1653 my ($order, $pdf_ref, $params) = @_;
1657 my $print_form = Form->new('');
1658 $print_form->{type} = $order->type;
1659 $print_form->{formname} = $params->{formname} || $order->type;
1660 $print_form->{format} = $params->{format} || 'pdf';
1661 $print_form->{media} = $params->{media} || 'file';
1662 $print_form->{groupitems} = $params->{groupitems};
1663 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1665 $order->language($params->{language});
1666 $order->flatten_to_form($print_form, format_amounts => 1);
1670 if ($print_form->{format} =~ /(opendocument|oasis)/i) {
1671 $template_ext = 'odt';
1672 $template_type = 'OpenDocument';
1675 # search for the template
1676 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1677 name => $print_form->{formname},
1678 extension => $template_ext,
1679 email => $print_form->{media} eq 'email',
1680 language => $params->{language},
1681 printer_id => $print_form->{printer_id}, # todo
1684 if (!defined $template_file) {
1685 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);
1688 return @errors if scalar @errors;
1690 $print_form->throw_on_error(sub {
1692 $print_form->prepare_for_printing;
1694 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1695 format => $print_form->{format},
1696 template_type => $template_type,
1697 template => $template_file,
1698 variables => $print_form,
1699 variable_content_types => {
1700 longdescription => 'html',
1701 partnotes => 'html',
1706 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1712 sub get_files_for_email_dialog {
1715 my %files = map { ($_ => []) } qw(versions files vc_files part_files);
1717 return %files if !$::instance_conf->get_doc_storage;
1719 if ($self->order->id) {
1720 $files{versions} = [ SL::File->get_all_versions(object_id => $self->order->id, object_type => $self->order->type, file_type => 'document') ];
1721 $files{files} = [ SL::File->get_all( object_id => $self->order->id, object_type => $self->order->type, file_type => 'attachment') ];
1722 $files{vc_files} = [ SL::File->get_all( object_id => $self->order->{$self->cv}->id, object_type => $self->cv, file_type => 'attachment') ];
1726 uniq_by { $_->{id} }
1728 +{ id => $_->part->id,
1729 partnumber => $_->part->partnumber }
1730 } @{$self->order->items_sorted};
1732 foreach my $part (@parts) {
1733 my @pfiles = SL::File->get_all(object_id => $part->{id}, object_type => 'part');
1734 push @{ $files{part_files} }, map { +{ %{ $_ }, partnumber => $part->{partnumber} } } @pfiles;
1737 foreach my $key (keys %files) {
1738 $files{$key} = [ sort_by { lc $_->{db_file}->{file_name} } @{ $files{$key} } ];
1744 sub make_periodic_invoices_config_from_yaml {
1745 my ($yaml_config) = @_;
1747 return if !$yaml_config;
1748 my $attr = YAML::Load($yaml_config);
1749 return if 'HASH' ne ref $attr;
1750 return SL::DB::PeriodicInvoicesConfig->new(%$attr);
1754 sub get_periodic_invoices_status {
1755 my ($self, $config) = @_;
1757 return if $self->type ne sales_order_type();
1758 return t8('not configured') if !$config;
1760 my $active = ('HASH' eq ref $config) ? $config->{active}
1761 : ('SL::DB::PeriodicInvoicesConfig' eq ref $config) ? $config->active
1762 : die "Cannot get status of periodic invoices config";
1764 return $active ? t8('active') : t8('inactive');
1768 my ($self, $action) = @_;
1770 return '' if none { lc($action)} qw(add edit);
1773 # $::locale->text("Add Sales Order");
1774 # $::locale->text("Add Purchase Order");
1775 # $::locale->text("Add Quotation");
1776 # $::locale->text("Add Request for Quotation");
1777 # $::locale->text("Edit Sales Order");
1778 # $::locale->text("Edit Purchase Order");
1779 # $::locale->text("Edit Quotation");
1780 # $::locale->text("Edit Request for Quotation");
1782 $action = ucfirst(lc($action));
1783 return $self->type eq sales_order_type() ? $::locale->text("$action Sales Order")
1784 : $self->type eq purchase_order_type() ? $::locale->text("$action Purchase Order")
1785 : $self->type eq sales_quotation_type() ? $::locale->text("$action Quotation")
1786 : $self->type eq request_quotation_type() ? $::locale->text("$action Request for Quotation")
1790 sub sales_order_type {
1794 sub purchase_order_type {
1798 sub sales_quotation_type {
1802 sub request_quotation_type {
1803 'request_quotation';
1807 return $_[0]->type eq sales_order_type() ? 'ordnumber'
1808 : $_[0]->type eq purchase_order_type() ? 'ordnumber'
1809 : $_[0]->type eq sales_quotation_type() ? 'quonumber'
1810 : $_[0]->type eq request_quotation_type() ? 'quonumber'
1822 SL::Controller::Order - controller for orders
1826 This is a new form to enter orders, completely rewritten with the use
1827 of controller and java script techniques.
1829 The aim is to provide the user a better expirience and a faster flow
1830 of work. Also the code should be more readable, more reliable and
1839 One input row, so that input happens every time at the same place.
1843 Use of pickers where possible.
1847 Possibility to enter more than one item at once.
1851 Item list in a scrollable area, so that the workflow buttons stay at
1856 Reordering item rows with drag and drop is possible. Sorting item rows is
1857 possible (by partnumber, description, qty, sellprice and discount for now).
1861 No C<update> is necessary. All entries and calculations are managed
1862 with ajax-calls and the page does only reload on C<save>.
1866 User can see changes immediately, because of the use of java script
1877 =item * C<SL/Controller/Order.pm>
1881 =item * C<template/webpages/order/form.html>
1885 =item * C<template/webpages/order/tabs/basic_data.html>
1887 Main tab for basic_data.
1889 This is the only tab here for now. "linked records" and "webdav" tabs are
1890 reused from generic code.
1894 =item * C<template/webpages/order/tabs/_business_info_row.html>
1896 For displaying information on business type
1898 =item * C<template/webpages/order/tabs/_item_input.html>
1900 The input line for items
1902 =item * C<template/webpages/order/tabs/_row.html>
1904 One row for already entered items
1906 =item * C<template/webpages/order/tabs/_tax_row.html>
1908 Displaying tax information
1910 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1912 Dialog for entering more than one item at once
1914 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1916 Results for the filter in the multi items dialog
1918 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1920 Dialog for selecting price and discount sources
1924 =item * C<js/kivi.Order.js>
1926 java script functions
1938 =item * credit limit
1940 =item * more workflows (quotation, rfq)
1942 =item * price sources: little symbols showing better price / better discount
1944 =item * select units in input row?
1946 =item * custom shipto address
1948 =item * check for direct delivery (workflow sales order -> purchase order)
1950 =item * language / part translations
1952 =item * access rights
1954 =item * display weights
1960 =item * optional client/user behaviour
1962 (transactions has to be set - department has to be set -
1963 force project if enabled in client config - transport cost reminder)
1967 =head1 KNOWN BUGS AND CAVEATS
1973 Customer discount is not displayed as a valid discount in price source popup
1974 (this might be a bug in price sources)
1976 (I cannot reproduce this (Bernd))
1980 No indication that <shift>-up/down expands/collapses second row.
1984 Inline creation of parts is not currently supported
1988 Table header is not sticky in the scrolling area.
1992 Sorting does not include C<position>, neither does reordering.
1994 This behavior was implemented intentionally. But we can discuss, which behavior
1995 should be implemented.
1999 C<show_multi_items_dialog> does not use the currently inserted string for
2004 The language selected in print or email dialog is not saved when the order is saved.
2008 =head1 To discuss / Nice to have
2014 How to expand/collapse second row. Now it can be done clicking the icon or
2019 Possibility to change longdescription in input row?
2023 Possibility to select PriceSources in input row?
2027 This controller uses a (changed) copy of the template for the PriceSource
2028 dialog. Maybe there could be used one code source.
2032 Rounding-differences between this controller (PriceTaxCalculator) and the old
2033 form. This is not only a problem here, but also in all parts using the PTC.
2034 There exists a ticket and a patch. This patch should be testet.
2038 An indicator, if the actual inputs are saved (like in an
2039 editor or on text processing application).
2043 A warning when leaving the page without saveing unchanged inputs.
2050 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>