1 package SL::Controller::Order;
4 use parent qw(SL::Controller::Base);
6 use SL::Helper::Flash qw(flash_later);
8 use SL::Locale::String qw(t8);
9 use SL::SessionFile::Random;
20 use SL::Helper::CreatePDF qw(:all);
21 use SL::Helper::PrintOptions;
23 use SL::Controller::Helper::GetModels;
25 use List::Util qw(first);
26 use List::MoreUtils qw(none pairwise first_index);
27 use English qw(-no_match_vars);
30 use Rose::Object::MakeMethods::Generic
32 scalar => [ qw(item_ids_to_delete) ],
33 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
38 __PACKAGE__->run_before('_check_auth');
40 __PACKAGE__->run_before('_recalc',
41 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
43 __PACKAGE__->run_before('_get_unalterable_data',
44 only => [ qw(save save_and_delivery_order print create_pdf send_email) ]);
54 $self->order->transdate(DateTime->now_local());
55 $self->order->reqdate(DateTime->today_local->next_workday) if !$self->order->reqdate;
60 title => $self->type eq _sales_order_type() ? $::locale->text('Add Sales Order')
61 : $self->type eq _purchase_order_type() ? $::locale->text('Add Purchase Order')
63 %{$self->{template_args}}
67 # edit an existing order
76 title => $self->type eq _sales_order_type() ? $::locale->text('Edit Sales Order')
77 : $self->type eq _purchase_order_type() ? $::locale->text('Edit Purchase Order')
79 %{$self->{template_args}}
87 my $errors = $self->_delete();
89 if (scalar @{ $errors }) {
90 $self->js->flash('error', $_) foreach @{ $errors };
91 return $self->js->render();
94 flash_later('info', $::locale->text('The order has been deleted'));
95 my @redirect_params = (
100 $self->redirect_to(@redirect_params);
107 my $errors = $self->_save();
109 if (scalar @{ $errors }) {
110 $self->js->flash('error', $_) foreach @{ $errors };
111 return $self->js->render();
114 flash_later('info', $::locale->text('The order has been saved'));
115 my @redirect_params = (
118 id => $self->order->id,
121 $self->redirect_to(@redirect_params);
126 # This is called if "print" is pressed in the print dialog.
127 # If PDF creation was requested and succeeded, the pdf is stored in a session
128 # file and the filename is stored as session value with an unique key. A
129 # javascript function with this key is then called. This function calls the
130 # download action below (action_download_pdf), which offers the file for
135 my $format = $::form->{print_options}->{format};
136 my $media = $::form->{print_options}->{media};
137 my $formname = $::form->{print_options}->{formname};
138 my $copies = $::form->{print_options}->{copies};
139 my $groupitems = $::form->{print_options}->{groupitems};
142 if (none { $format eq $_ } qw(pdf)) {
143 return $self->js->flash('error', t8('Format \'#1\' is not supported yet/anymore.', $format))->render;
146 # only screen or printer by now
147 if (none { $media eq $_ } qw(screen printer)) {
148 return $self->js->flash('error', t8('Media \'#1\' is not supported yet/anymore.', $media))->render;
152 $language = SL::DB::Language->new(id => $::form->{print_options}->{language_id})->load if $::form->{print_options}->{language_id};
154 my $form = Form->new;
155 $form->{ordnumber} = $self->order->ordnumber;
156 $form->{type} = $self->type;
157 $form->{format} = $format;
158 $form->{formname} = $formname;
159 $form->{language} = '_' . $language->template_code if $language;
160 my $pdf_filename = $form->generate_attachment_filename();
163 my @errors = _create_pdf($self->order, \$pdf, { format => $format,
164 formname => $formname,
165 language => $language,
166 groupitems => $groupitems });
167 if (scalar @errors) {
168 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render;
171 if ($media eq 'screen') {
173 my $sfile = SL::SessionFile::Random->new(mode => "w");
174 $sfile->fh->print($pdf);
177 my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
178 $::auth->set_session_value("Order::create_pdf-${key}" => $sfile->file_name);
181 ->run('kivi.Order.download_pdf', $pdf_filename, $key)
182 ->flash('info', t8('The PDF has been created'));
184 } elsif ($media eq 'printer') {
186 my $printer_id = $::form->{print_options}->{printer_id};
187 SL::DB::Printer->new(id => $printer_id)->load->print_document(
192 $self->js->flash('info', t8('The PDF has been printed'));
195 # copy file to webdav folder
196 if ($self->order->ordnumber && $::instance_conf->get_webdav_documents) {
197 my $webdav = SL::Webdav->new(
199 number => $self->order->ordnumber,
201 my $webdav_file = SL::Webdav::File->new(
203 filename => $pdf_filename,
206 $webdav_file->store(data => \$pdf);
209 $self->js->flash('error', t8('Storing PDF to webdav folder failed: #1', $@));
216 # offer pdf for download
218 # It needs to get the key for the session value to get the pdf file.
219 sub action_download_pdf {
222 my $key = $::form->{key};
223 my $tmp_filename = $::auth->get_session_value("Order::create_pdf-${key}");
224 return $self->send_file(
226 type => 'application/pdf',
227 name => $::form->{pdf_filename},
231 # open the email dialog
232 sub action_show_email_dialog {
235 my $cv_method = $self->cv;
237 if (!$self->order->$cv_method) {
238 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'))
242 $self->{email}->{to} = $self->order->contact->cp_email if $self->order->contact;
243 $self->{email}->{to} ||= $self->order->$cv_method->email;
244 $self->{email}->{cc} = $self->order->$cv_method->cc;
245 $self->{email}->{bcc} = join ', ', grep $_, $self->order->$cv_method->bcc, SL::DB::Default->get->global_bcc;
246 # Todo: get addresses from shipto, if any
248 my $form = Form->new;
249 $form->{ordnumber} = $self->order->ordnumber;
250 $form->{formname} = $self->type;
251 $form->{type} = $self->type;
252 $form->{language} = 'de';
253 $form->{format} = 'pdf';
255 $self->{email}->{subject} = $form->generate_email_subject();
256 $self->{email}->{attachment_filename} = $form->generate_attachment_filename();
257 $self->{email}->{message} = $form->create_email_signature();
259 my $dialog_html = $self->render('order/tabs/_email_dialog', { output => 0 });
261 ->run('kivi.Order.show_email_dialog', $dialog_html)
268 # Todo: handling error messages: flash is not displayed in dialog, but in the main form
269 sub action_send_email {
272 my $mail = Mailer->new;
273 $mail->{from} = qq|"$::myconfig{name}" <$::myconfig{email}>|;
274 $mail->{$_} = $::form->{email}->{$_} for qw(to cc bcc subject message);
277 my @errors = _create_pdf($self->order, \$pdf, {media => 'email'});
278 if (scalar @errors) {
279 return $self->js->flash('error', t8('Conversion to PDF failed: #1', $errors[0]))->render($self);
282 $mail->{attachments} = [{ "content" => $pdf,
283 "name" => $::form->{email}->{attachment_filename} }];
285 if (my $err = $mail->send) {
286 return $self->js->flash('error', t8('Sending E-mail: ') . $err)
291 my $intnotes = $self->order->intnotes;
292 $intnotes .= "\n\n" if $self->order->intnotes;
293 $intnotes .= t8('[email]') . "\n";
294 $intnotes .= t8('Date') . ": " . $::locale->format_date_object(DateTime->now_local, precision => 'seconds') . "\n";
295 $intnotes .= t8('To (email)') . ": " . $mail->{to} . "\n";
296 $intnotes .= t8('Cc') . ": " . $mail->{cc} . "\n" if $mail->{cc};
297 $intnotes .= t8('Bcc') . ": " . $mail->{bcc} . "\n" if $mail->{bcc};
298 $intnotes .= t8('Subject') . ": " . $mail->{subject} . "\n\n";
299 $intnotes .= t8('Message') . ": " . $mail->{message};
302 ->val('#order_intnotes', $intnotes)
303 ->run('kivi.Order.close_email_dialog')
307 # save the order and redirect to the frontend subroutine for a new
309 sub action_save_and_delivery_order {
312 my $errors = $self->_save();
314 if (scalar @{ $errors }) {
315 $self->js->flash('error', $_) foreach @{ $errors };
316 return $self->js->render();
318 flash_later('info', $::locale->text('The order has been saved'));
320 my @redirect_params = (
321 controller => 'oe.pl',
322 action => 'oe_delivery_order_from_order',
323 id => $self->order->id,
326 $self->redirect_to(@redirect_params);
329 # set form elements in respect to a changed customer or vendor
331 # This action is called on an change of the customer/vendor picker.
332 sub action_customer_vendor_changed {
335 my $cv_method = $self->cv;
337 if ($self->order->$cv_method->contacts && scalar @{ $self->order->$cv_method->contacts } > 0) {
338 $self->js->show('#cp_row');
340 $self->js->hide('#cp_row');
343 if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
344 $self->js->show('#shipto_row');
346 $self->js->hide('#shipto_row');
349 $self->order->taxzone_id($self->order->$cv_method->taxzone_id);
351 if ($self->order->is_sales) {
352 $self->order->taxincluded(defined($self->order->$cv_method->taxincluded_checked)
353 ? $self->order->$cv_method->taxincluded_checked
354 : $::myconfig{taxincluded_checked});
357 $self->order->payment_id($self->order->$cv_method->payment_id);
358 $self->order->delivery_term_id($self->order->$cv_method->delivery_term_id);
363 ->replaceWith('#order_cp_id', $self->build_contact_select)
364 ->replaceWith('#order_shipto_id', $self->build_shipto_select)
365 ->val( '#order_taxzone_id', $self->order->taxzone_id)
366 ->val( '#order_taxincluded', $self->order->taxincluded)
367 ->val( '#order_payment_id', $self->order->payment_id)
368 ->val( '#order_delivery_term_id', $self->order->delivery_term_id)
369 ->val( '#order_intnotes', $self->order->$cv_method->notes)
370 ->focus( '#order_' . $self->cv . '_id');
372 $self->_js_redisplay_amounts_and_taxes;
376 # called if a unit in an existing item row is changed
377 sub action_unit_changed {
380 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
381 my $item = $self->order->items_sorted->[$idx];
383 my $old_unit_obj = SL::DB::Unit->new(name => $::form->{old_unit})->load;
384 $item->sellprice($item->unit_obj->convert_to($item->sellprice, $old_unit_obj));
389 ->run('kivi.Order.update_sellprice', $::form->{item_id}, $item->sellprice_as_number);
390 $self->_js_redisplay_line_values;
391 $self->_js_redisplay_amounts_and_taxes;
395 # add an item row for a new item entered in the input row
396 sub action_add_item {
399 my $form_attr = $::form->{add_item};
401 return unless $form_attr->{parts_id};
403 my $item = _new_item($self->order, $form_attr);
405 $self->order->add_items($item);
409 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
410 my $row_as_html = $self->p->render('order/tabs/_row',
413 ALL_PRICE_FACTORS => $self->all_price_factors
417 ->append('#row_table_id', $row_as_html);
419 if ( $item->part->is_assortment ) {
420 $form_attr->{qty_as_number} = 1 unless $form_attr->{qty_as_number};
421 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
422 my $attr = { parts_id => $assortment_item->parts_id,
423 qty => $assortment_item->qty * $::form->parse_amount(\%::myconfig, $form_attr->{qty_as_number}), # TODO $form_attr->{unit}
424 unit => $assortment_item->unit,
425 description => $assortment_item->part->description,
427 my $item = _new_item($self->order, $attr);
429 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
430 $item->discount(1) unless $assortment_item->charge;
432 $self->order->add_items( $item );
434 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
435 my $row_as_html = $self->p->render('order/tabs/_row',
438 ALL_PRICE_FACTORS => $self->all_price_factors
441 ->append('#row_table_id', $row_as_html);
446 ->val('.add_item_input', '')
447 ->run('kivi.Order.init_row_handlers')
448 ->run('kivi.Order.row_table_scroll_down')
449 ->run('kivi.Order.renumber_positions')
450 ->focus('#add_item_parts_id_name');
452 $self->_js_redisplay_amounts_and_taxes;
456 # open the dialog for entering multiple items at once
457 sub action_show_multi_items_dialog {
458 require SL::DB::PartsGroup;
459 $_[0]->render('order/tabs/_multi_items_dialog', { layout => 0 },
460 all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
463 # update the filter results in the multi item dialog
464 sub action_multi_items_update_result {
467 $::form->{multi_items}->{filter}->{obsolete} = 0;
469 my $count = $_[0]->multi_items_models->count;
472 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
473 $_[0]->render($text, { layout => 0 });
474 } elsif ($count > $max_count) {
475 my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
476 $_[0]->render($text, { layout => 0 });
478 my $multi_items = $_[0]->multi_items_models->get;
479 $_[0]->render('order/tabs/_multi_items_result', { layout => 0 },
480 multi_items => $multi_items);
484 # add item rows for multiple items at once
485 sub action_add_multi_items {
488 my @form_attr = grep { $_->{qty_as_number} } @{ $::form->{add_multi_items} };
489 return $self->js->render() unless scalar @form_attr;
492 foreach my $attr (@form_attr) {
493 my $item = _new_item($self->order, $attr);
495 if ( $item->part->is_assortment ) {
496 foreach my $assortment_item ( @{$item->part->assortment_items} ) {
497 my $attr = { parts_id => $assortment_item->parts_id,
498 qty => $assortment_item->qty * $item->qty, # TODO $form_attr->{unit}
499 unit => $assortment_item->unit,
500 description => $assortment_item->part->description,
502 my $item = _new_item($self->order, $attr);
504 # set discount to 100% if item isn't supposed to be charged, overwriting any customer discount
505 $item->discount(1) unless $assortment_item->charge;
506 push @items, $assortment_item;
510 $self->order->add_items(@items);
514 foreach my $item (@items) {
515 my $item_id = join('_', 'new', Time::HiRes::gettimeofday(), int rand 1000000000000);
516 my $row_as_html = $self->p->render('order/tabs/_row',
519 ALL_PRICE_FACTORS => $self->all_price_factors
522 $self->js->append('#row_table_id', $row_as_html);
526 ->run('kivi.Order.close_multi_items_dialog')
527 ->run('kivi.Order.init_row_handlers')
528 ->run('kivi.Order.row_table_scroll_down')
529 ->run('kivi.Order.renumber_positions')
530 ->focus('#add_item_parts_id_name');
532 $self->_js_redisplay_amounts_and_taxes;
536 # recalculate all linetotals, amounts and taxes and redisplay them
537 sub action_recalc_amounts_and_taxes {
542 $self->_js_redisplay_line_values;
543 $self->_js_redisplay_amounts_and_taxes;
547 # redisplay item rows if they are sorted by an attribute
548 sub action_reorder_items {
552 partnumber => sub { $_[0]->part->partnumber },
553 description => sub { $_[0]->description },
554 qty => sub { $_[0]->qty },
555 sellprice => sub { $_[0]->sellprice },
556 discount => sub { $_[0]->discount },
559 my $method = $sort_keys{$::form->{order_by}};
560 my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @{ $self->order->items_sorted };
561 if ($::form->{sort_dir}) {
562 @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
564 @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
567 ->run('kivi.Order.redisplay_items', \@to_sort)
571 # show the popup to choose a price/discount source
572 sub action_price_popup {
575 my $idx = first_index { $_ eq $::form->{item_id} } @{ $::form->{orderitem_ids} };
576 my $item = $self->order->items_sorted->[$idx];
578 $self->render_price_dialog($item);
581 # get the longdescription for an item if the dialog to enter/change the
582 # longdescription was opened and the longdescription is empty
584 # If this item is new, get the longdescription from Part.
585 # Otherwise get it from OrderItem.
586 sub action_get_item_longdescription {
589 if ($::form->{item_id}) {
590 $longdescription = SL::DB::OrderItem->new(id => $::form->{item_id})->load->longdescription;
591 } elsif ($::form->{parts_id}) {
592 $longdescription = SL::DB::Part->new(id => $::form->{parts_id})->load->notes;
594 $_[0]->render(\ $longdescription, { type => 'text' });
597 # load the second row for one or more items
599 # This action gets the html code for all items second rows by rendering a template for
600 # the second row and sets the html code via client js.
601 sub action_load_second_rows {
604 $self->_recalc() if $self->order->is_sales; # for margin calculation
606 foreach my $item_id (@{ $::form->{item_ids} }) {
607 my $idx = first_index { $_ eq $item_id } @{ $::form->{orderitem_ids} };
608 my $item = $self->order->items_sorted->[$idx];
610 $self->_js_load_second_row($item, $item_id, 0);
613 $self->js->run('kivi.Order.init_row_handlers') if $self->order->is_sales; # for lastcosts change-callback
618 sub _js_load_second_row {
619 my ($self, $item, $item_id, $do_parse) = @_;
622 # Parse values from form (they are formated while rendering (template)).
623 # Workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
624 # This parsing is not necessary at all, if we assure that the second row/cvars are only loaded once.
625 foreach my $var (@{ $item->cvars_by_config }) {
626 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
628 $item->parse_custom_variable_values;
631 my $row_as_html = $self->p->render('order/tabs/_second_row', ITEM => $item);
634 ->html('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', $row_as_html)
635 ->data('.row_entry:has(#item_' . $item_id . ') [name = "second_row"]', 'loaded', 1);
638 sub _js_redisplay_line_values {
641 my $is_sales = $self->order->is_sales;
643 # sales orders with margins
648 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
649 $::form->format_amount(\%::myconfig, $_->{marge_total}, 2, 0),
650 $::form->format_amount(\%::myconfig, $_->{marge_percent}, 2, 0),
651 ]} @{ $self->order->items_sorted };
655 $::form->format_amount(\%::myconfig, $_->{linetotal}, 2, 0),
656 ]} @{ $self->order->items_sorted };
660 ->run('kivi.Order.redisplay_line_values', $is_sales, \@data);
663 sub _js_redisplay_amounts_and_taxes {
666 if (scalar @{ $self->{taxes} }) {
667 $self->js->show('#taxincluded_row_id');
669 $self->js->hide('#taxincluded_row_id');
672 if ($self->order->taxincluded) {
673 $self->js->hide('#subtotal_row_id');
675 $self->js->show('#subtotal_row_id');
679 ->html('#netamount_id', $::form->format_amount(\%::myconfig, $self->order->netamount, -2))
680 ->html('#amount_id', $::form->format_amount(\%::myconfig, $self->order->amount, -2))
682 ->insertBefore($self->build_tax_rows, '#amount_row_id');
689 sub init_valid_types {
690 [ _sales_order_type(), _purchase_order_type() ];
696 if (none { $::form->{type} eq $_ } @{$self->valid_types}) {
697 die "Not a valid type for order";
700 $self->type($::form->{type});
706 my $cv = $self->type eq _sales_order_type() ? 'customer'
707 : $self->type eq _purchase_order_type() ? 'vendor'
708 : die "Not a valid type for order";
721 # model used to filter/display the parts in the multi-items dialog
722 sub init_multi_items_models {
723 SL::Controller::Helper::GetModels->new(
726 with_objects => [ qw(unit_obj) ],
727 disable_plugin => 'paginated',
728 source => $::form->{multi_items},
734 partnumber => t8('Partnumber'),
735 description => t8('Description')}
739 sub init_all_price_factors {
740 SL::DB::Manager::PriceFactor->get_all;
746 my $right_for = { map { $_ => $_.'_edit' } @{$self->valid_types} };
748 my $right = $right_for->{ $self->type };
749 $right ||= 'DOES_NOT_EXIST';
751 $::auth->assert($right);
754 # build the selection box for contacts
756 # Needed, if customer/vendor changed.
757 sub build_contact_select {
760 $self->p->select_tag('order.cp_id', [ $self->order->{$self->cv}->contacts ],
761 value_key => 'cp_id',
762 title_key => 'full_name_dep',
763 default => $self->order->cp_id,
765 style => 'width: 300px',
769 # build the selection box for shiptos
771 # Needed, if customer/vendor changed.
772 sub build_shipto_select {
775 $self->p->select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
776 value_key => 'shipto_id',
777 title_key => 'displayable_id',
778 default => $self->order->shipto_id,
780 style => 'width: 300px',
784 # build the rows for displaying taxes
786 # Called if amounts where recalculated and redisplayed.
791 foreach my $tax (sort { $a->{tax}->rate cmp $b->{tax}->rate } @{ $self->{taxes} }) {
792 $rows_as_html .= $self->p->render('order/tabs/_tax_row', TAX => $tax, TAXINCLUDED => $self->order->taxincluded);
794 return $rows_as_html;
798 sub render_price_dialog {
799 my ($self, $record_item) = @_;
801 my $price_source = SL::PriceSource->new(record_item => $record_item, record => $self->order);
805 'kivi.io.price_chooser_dialog',
806 t8('Available Prices'),
807 $self->render('order/tabs/_price_sources_dialog', { output => 0 }, price_source => $price_source)
812 # $self->js->text('#dialog_flash_error_content', join ' ', @errors);
813 # $self->js->show('#dialog_flash_error');
822 return if !$::form->{id};
824 $self->order(SL::DB::Manager::Order->find_by(id => $::form->{id}));
827 # load or create a new order object
829 # And assign changes from the for to this object.
830 # If the order is loaded from db, check if items are deleted in the form,
831 # remove them form the object and collect them for removing from db on saving.
832 # Then create/update items from form (via _make_item) and add them.
836 # add_items adds items to an order with no items for saving, but they cannot
837 # be retrieved via items until the order is saved. Adding empty items to new
838 # order here solves this problem.
840 $order = SL::DB::Manager::Order->find_by(id => $::form->{id}) if $::form->{id};
841 $order ||= SL::DB::Order->new(orderitems => []);
843 my $form_orderitems = delete $::form->{order}->{orderitems};
844 $order->assign_attributes(%{$::form->{order}});
846 # remove deleted items
847 $self->item_ids_to_delete([]);
848 foreach my $idx (reverse 0..$#{$order->orderitems}) {
849 my $item = $order->orderitems->[$idx];
850 if (none { $item->id == $_->{id} } @{$form_orderitems}) {
851 splice @{$order->orderitems}, $idx, 1;
852 push @{$self->item_ids_to_delete}, $item->id;
858 foreach my $form_attr (@{$form_orderitems}) {
859 my $item = _make_item($order, $form_attr);
860 $item->position($pos);
864 $order->add_items(grep {!$_->id} @items);
869 # create or update items from form
871 # Make item objects from form values. For items already existing read from db.
872 # Create a new item else. And assign attributes.
874 my ($record, $attr) = @_;
877 $item = first { $_->id == $attr->{id} } @{$record->items} if $attr->{id};
881 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
882 # they cannot be retrieved via custom_variables until the order/orderitem is
883 # saved. Adding empty custom_variables to new orderitem here solves this problem.
884 $item ||= SL::DB::OrderItem->new(custom_variables => []);
886 $item->assign_attributes(%$attr);
887 $item->longdescription($item->part->notes) if $is_new && !defined $attr->{longdescription};
888 $item->project_id($record->globalproject_id) if $is_new && !defined $attr->{project_id};
889 $item->lastcost($item->part->lastcost) if $is_new && !defined $attr->{lastcost_as_number};
896 # This is used to add one item
898 my ($record, $attr) = @_;
900 my $item = SL::DB::OrderItem->new;
901 $item->assign_attributes(%$attr);
903 my $part = SL::DB::Part->new(id => $attr->{parts_id})->load;
904 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
906 $item->unit($part->unit) if !$item->unit;
909 if ( $part->is_assortment ) {
910 # add assortment items with price 0, as the components carry the price
911 $price_src = $price_source->price_from_source("");
912 $price_src->price(0);
913 } elsif ($item->sellprice) {
914 $price_src = $price_source->price_from_source("");
915 $price_src->price($item->sellprice);
917 $price_src = $price_source->best_price
918 ? $price_source->best_price
919 : $price_source->price_from_source("");
920 $price_src->price(0) if !$price_source->best_price;
924 if ($item->discount) {
925 $discount_src = $price_source->discount_from_source("");
926 $discount_src->discount($item->discount);
928 $discount_src = $price_source->best_discount
929 ? $price_source->best_discount
930 : $price_source->discount_from_source("");
931 $discount_src->discount(0) if !$price_source->best_discount;
935 $new_attr{part} = $part;
936 $new_attr{description} = $part->description if ! $item->description;
937 $new_attr{qty} = 1.0 if ! $item->qty;
938 $new_attr{price_factor_id} = $part->price_factor_id if ! $item->price_factor_id;
939 $new_attr{sellprice} = $price_src->price;
940 $new_attr{discount} = $discount_src->discount;
941 $new_attr{active_price_source} = $price_src;
942 $new_attr{active_discount_source} = $discount_src;
943 $new_attr{longdescription} = $part->notes if ! defined $attr->{longdescription};
944 $new_attr{project_id} = $record->globalproject_id;
945 $new_attr{lastcost} = $part->lastcost;
947 # add_custom_variables adds cvars to an orderitem with no cvars for saving, but
948 # they cannot be retrieved via custom_variables until the order/orderitem is
949 # saved. Adding empty custom_variables to new orderitem here solves this problem.
950 $new_attr{custom_variables} = [];
952 $item->assign_attributes(%new_attr);
957 # recalculate prices and taxes
959 # Using the PriceTaxCalculator. Store linetotals in the item objects.
963 # bb: todo: currency later
964 $self->order->currency_id($::instance_conf->get_currency_id());
966 my %pat = $self->order->calculate_prices_and_taxes();
968 foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
969 my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
971 my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
972 push(@{ $self->{taxes} }, { amount => $pat{taxes}->{$tax_chart_id},
973 netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
977 pairwise { $a->{linetotal} = $b->{linetotal} } @{$self->order->items}, @{$pat{items}};
980 # get data for saving, printing, ..., that is not changed in the form
982 # Only cvars for now.
983 sub _get_unalterable_data {
986 foreach my $item (@{ $self->order->items }) {
987 # autovivify all cvars that are not in the form (cvars_by_config can do it).
988 # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
989 foreach my $var (@{ $item->cvars_by_config }) {
990 $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
992 $item->parse_custom_variable_values;
998 # And remove related files in the spool directory
1003 my $db = $self->order->db;
1005 $db->with_transaction(
1007 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $self->order->id ]) };
1008 $self->order->delete;
1009 my $spool = $::lx_office_conf{paths}->{spool};
1010 unlink map { "$spool/$_" } @spoolfiles if $spool;
1013 }) || push(@{$errors}, $db->error);
1020 # And delete items that are deleted in the form.
1025 my $db = $self->order->db;
1027 $db->with_transaction(sub {
1028 SL::DB::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete};
1029 $self->order->save(cascade => 1);
1030 }) || push(@{$errors}, $db->error);
1039 $self->{all_taxzones} = SL::DB::Manager::TaxZone->get_all_sorted();
1040 $self->{all_departments} = SL::DB::Manager::Department->get_all_sorted();
1041 $self->{all_employees} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
1044 $self->{all_salesmen} = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->salesman_id,
1047 $self->{all_projects} = SL::DB::Manager::Project->get_all(where => [ or => [ id => $self->order->globalproject_id,
1049 sort_by => 'projectnumber');
1050 $self->{all_payment_terms} = SL::DB::Manager::PaymentTerm->get_all_sorted(where => [ or => [ id => $self->order->payment_id,
1053 $self->{all_delivery_terms} = SL::DB::Manager::DeliveryTerm->get_all_sorted();
1055 $self->{current_employee_id} = SL::DB::Manager::Employee->current->id;
1057 my $print_form = Form->new('');
1058 $print_form->{type} = $self->type;
1059 $print_form->{printers} = SL::DB::Manager::Printer->get_all_sorted;
1060 $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
1061 $self->{print_options} = SL::Helper::PrintOptions->get_print_options(
1062 form => $print_form,
1063 options => {dialog_name_prefix => 'print_options.',
1067 no_opendocument => 1,
1071 foreach my $item (@{$self->order->orderitems}) {
1072 my $price_source = SL::PriceSource->new(record_item => $item, record => $self->order);
1073 $item->active_price_source( $price_source->price_from_source( $item->active_price_source ));
1074 $item->active_discount_source($price_source->discount_from_source($item->active_discount_source));
1077 if ($self->order->ordnumber && $::instance_conf->get_webdav) {
1078 my $webdav = SL::Webdav->new(
1079 type => $self->type,
1080 number => $self->order->ordnumber,
1082 my $webdav_path = $webdav->webdav_path;
1083 my @all_objects = $webdav->get_all_objects;
1084 @{ $self->{template_args}->{WEBDAV} } = map { { name => $_->filename,
1086 link => File::Spec->catfile($_->full_filedescriptor),
1090 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order ckeditor/ckeditor ckeditor/adapters/jquery);
1094 my ($order, $pdf_ref, $params) = @_;
1098 my $print_form = Form->new('');
1099 $print_form->{type} = $order->type;
1100 $print_form->{formname} = $params->{formname} || $order->type;
1101 $print_form->{format} = $params->{format} || 'pdf';
1102 $print_form->{media} = $params->{media} || 'file';
1103 $print_form->{groupitems} = $params->{groupitems};
1104 $print_form->{media} = 'file' if $print_form->{media} eq 'screen';
1105 $print_form->{language} = $params->{language}->template_code if $print_form->{language};
1106 $print_form->{language_id} = $params->{language}->id if $print_form->{language};
1108 $order->flatten_to_form($print_form, format_amounts => 1);
1110 # search for the template
1111 my ($template_file, @template_files) = SL::Helper::CreatePDF->find_template(
1112 name => $print_form->{formname},
1113 email => $print_form->{media} eq 'email',
1114 language => $params->{language},
1115 printer_id => $print_form->{printer_id}, # todo
1118 if (!defined $template_file) {
1119 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);
1122 return @errors if scalar @errors;
1124 $print_form->throw_on_error(sub {
1126 $print_form->prepare_for_printing;
1128 $$pdf_ref = SL::Helper::CreatePDF->create_pdf(
1129 template => $template_file,
1130 variables => $print_form,
1131 variable_content_types => {
1132 longdescription => 'html',
1133 partnotes => 'html',
1138 } || push @errors, ref($EVAL_ERROR) eq 'SL::X::FormError' ? $EVAL_ERROR->getMessage : $EVAL_ERROR;
1144 sub _sales_order_type {
1148 sub _purchase_order_type {
1160 SL::Controller::Order - controller for orders
1164 This is a new form to enter orders, completely rewritten with the use
1165 of controller and java script techniques.
1167 The aim is to provide the user a better expirience and a faster flow
1168 of work. Also the code should be more readable, more reliable and
1177 One input row, so that input happens every time at the same place.
1181 Use of pickers where possible.
1185 Possibility to enter more than one item at once.
1189 Save order only on "save" (and "save and delivery order"-workflow). No
1190 hidden save on "print" or "email".
1194 Item list in a scrollable area, so that the workflow buttons stay at
1199 Reordering item rows with drag and drop is possible. Sorting item rows is
1200 possible (by partnumber, description, qty, sellprice and discount for now).
1204 No C<update> is necessary. All entries and calculations are managed
1205 with ajax-calls and the page does only reload on C<save>.
1209 User can see changes immediately, because of the use of java script
1220 =item * C<SL/Controller/Order.pm>
1224 =item * C<template/webpages/order/form.html>
1228 =item * C<template/webpages/order/tabs/basic_data.html>
1230 Main tab for basic_data.
1232 This is the only tab here for now. "linked records" and "webdav" tabs are
1233 reused from generic code.
1237 =item * C<template/webpages/order/tabs/_item_input.html>
1239 The input line for items
1241 =item * C<template/webpages/order/tabs/_row.html>
1243 One row for already entered items
1245 =item * C<template/webpages/order/tabs/_tax_row.html>
1247 Displaying tax information
1249 =item * C<template/webpages/order/tabs/_multi_items_dialog.html>
1251 Dialog for entering more than one item at once
1253 =item * C<template/webpages/order/tabs/_multi_items_result.html>
1255 Results for the filter in the multi items dialog
1257 =item * C<template/webpages/order/tabs/_price_sources_dialog.html>
1259 Dialog for selecting price and discount sources
1261 =item * C<template/webpages/order/tabs/_email_dialog.html>
1267 =item * C<js/kivi.Order.js>
1269 java script functions
1281 =item * customer/vendor details ('D'-button)
1283 =item * credit limit
1285 =item * more workflows (save as new / invoice)
1287 =item * price sources: little symbols showing better price / better discount
1289 =item * custom shipto address
1291 =item * periodic invoices
1293 =item * language / part translations
1295 =item * access rights
1297 =item * preset salesman from customer
1299 =item * display weights
1301 =item * force project if enabled in client config
1309 =head1 KNOWN BUGS AND CAVEATS
1315 Customer discount is not displayed as a valid discount in price source popup
1316 (this might be a bug in price sources)
1320 No indication that <shift>-up/down expands/collapses second row.
1324 Inline creation of parts is not currently supported
1328 Table header is not sticky in the scrolling area.
1332 Sorting does not include C<position>, neither does reordering.
1334 This behavior was implemented intentionally. But we can discuss, which behavior
1335 should be implemented.
1339 C<show_multi_items_dialog> does not use the currently inserted string for
1346 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>