1 package SL::Controller::Inventory;
5 use POSIX qw(strftime);
7 use parent qw(SL::Controller::Base);
10 use SL::DB::Stocktaking;
12 use SL::DB::Warehouse;
16 use SL::ReportGenerator;
17 use SL::Locale::String qw(t8);
18 use SL::Presenter::Tag qw(select_tag);
20 use SL::Helper::Flash;
21 use SL::Controller::Helper::ReportGenerator;
22 use SL::Controller::Helper::GetModels;
24 use English qw(-no_match_vars);
26 use Rose::Object::MakeMethods::Generic (
27 'scalar --get_set_init' => [ qw(warehouses units is_stocktaking stocktaking_models stocktaking_cutoff_date) ],
28 'scalar' => [ qw(warehouse bin unit part) ],
31 __PACKAGE__->run_before('_check_auth');
32 __PACKAGE__->run_before('_check_warehouses');
33 __PACKAGE__->run_before('load_part_from_form', only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed save_stocktaking) ]);
34 __PACKAGE__->run_before('load_unit_from_form', only => [ qw(stock_in part_changed mini_stock stock stocktaking_part_changed save_stocktaking) ]);
35 __PACKAGE__->run_before('load_wh_from_form', only => [ qw(stock_in warehouse_changed stock stocktaking save_stocktaking) ]);
36 __PACKAGE__->run_before('load_bin_from_form', only => [ qw(stock_in stock stocktaking save_stocktaking) ]);
37 __PACKAGE__->run_before('set_target_from_part', only => [ qw(part_changed) ]);
38 __PACKAGE__->run_before('mini_stock', only => [ qw(stock_in mini_stock) ]);
39 __PACKAGE__->run_before('sanitize_target', only => [ qw(stock_usage stock_in warehouse_changed part_changed stocktaking stocktaking_part_changed save_stocktaking) ]);
40 __PACKAGE__->run_before('set_layout');
45 $::form->{title} = t8('Stock');
47 $::request->layout->focus('#part_id_name');
48 my $transfer_types = WH->retrieve_transfer_types('in');
49 map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
50 $self->setup_stock_in_action_bar;
51 $self->render('inventory/warehouse_selection_stock', title => $::form->{title}, TRANSFER_TYPES => $transfer_types );
54 sub action_stock_usage {
57 $::form->{title} = t8('UsageE');
59 $::form->get_lists('warehouses' => { 'key' => 'WAREHOUSES',
61 $::request->layout->use_javascript("${_}.js") for qw(kivi.PartsWarehouse);
63 $self->setup_stock_usage_action_bar;
64 $self->render('inventory/warehouse_usage',
65 title => $::form->{title},
66 year => DateTime->today->year,
67 WAREHOUSES => $::form->{WAREHOUSES},
68 WAREHOUSE_FILTER => 1,
77 return qw(stock incorrection found insum back outcorrection disposed
78 missing shipped used outsum consumed averconsumed);
84 $main::lxdebug->enter_sub();
86 my $form = $main::form;
87 my %myconfig = %main::myconfig;
88 my $locale = $main::locale;
90 $form->{title} = t8('UsageE');
91 $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
93 my $report = SL::ReportGenerator->new(\%myconfig, $form);
95 my @columns = qw(partnumber partdescription);
97 push @columns , qw(ptype unit) if $form->{report_generator_output_format} eq 'HTML';
99 my @numcolumns = qw(stock incorrection found insum back outcorrection disposed
100 missing shipped used outsum consumed averconsumed);
102 push @columns , $self->getnumcolumns();
104 my @hidden_variables = qw(reporttype year duetyp fromdate todate
105 warehouse_id bin_id partnumber description bestbefore chargenumber partstypes_id);
107 'partnumber' => { 'text' => $locale->text('Part Number'), },
108 'partdescription' => { 'text' => $locale->text('Part_br_Description'), },
109 'unit' => { 'text' => $locale->text('Unit'), },
110 'stock' => { 'text' => $locale->text('stock_br'), },
111 'incorrection' => { 'text' => $locale->text('correction_br'), },
112 'found' => { 'text' => $locale->text('found_br'), },
113 'insum' => { 'text' => $locale->text('sum'), },
114 'back' => { 'text' => $locale->text('back_br'), },
115 'outcorrection' => { 'text' => $locale->text('correction_br'), },
116 'disposed' => { 'text' => $locale->text('disposed_br'), },
117 'missing' => { 'text' => $locale->text('missing_br'), },
118 'shipped' => { 'text' => $locale->text('shipped_br'), },
119 'used' => { 'text' => $locale->text('used_br'), },
120 'outsum' => { 'text' => $locale->text('sum'), },
121 'consumed' => { 'text' => $locale->text('consumed'), },
122 'averconsumed' => { 'text' => $locale->text('averconsumed_br'), },
126 map { $column_defs{$_}->{visible} = 1 } @columns;
127 #map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
128 map { $column_defs{$_}->{align} = 'right' } @numcolumns;
130 my @custom_headers = ();
132 push @custom_headers, [
133 { 'text' => $locale->text('Part'),
134 'colspan' => ($form->{report_generator_output_format} eq 'HTML'?4:2), 'align' => 'center'},
135 { 'text' => $locale->text('Into bin'), 'colspan' => 4, 'align' => 'center'},
136 { 'text' => $locale->text('From bin'), 'colspan' => 7, 'align' => 'center'},
137 { 'text' => $locale->text('UsageWithout'), 'colspan' => 2, 'align' => 'center'},
142 map { push @line_2 , $column_defs{$_} } @columns;
143 push @custom_headers, [ @line_2 ];
145 $report->set_custom_headers(@custom_headers);
146 $report->set_columns( %column_defs );
147 $report->set_column_order(@columns);
149 $report->set_export_options('usage', @hidden_variables );
151 $report->set_sort_indicator($form->{sort}, $form->{order});
152 $report->set_options('output_format' => 'HTML',
153 'controller_class' => 'Inventory',
154 'title' => $form->{title},
155 # 'html_template' => 'inventory/usage_report',
156 'attachment_basename' => strftime($locale->text('warehouse_usage_list') . '_%Y%m%d', localtime time));
157 $report->set_options_from_form;
161 # reporttype = custom
165 my $start = DateTime->now_local;
166 my $end = DateTime->now_local;
167 my $actualepoch = $end->epoch();
170 $searchparams{reporttype} = $form->{reporttype};
171 if ($form->{reporttype} eq "custom") {
176 #forgotten the year --> thisyear
177 if ($form->{year} !~ m/^\d\d\d\d$/) {
178 $locale->date(\%myconfig, $form->current_date(\%myconfig), 0) =~
182 my $leapday = ($form->{year} % 4 == 0) ? 1:0;
184 if ($form->{duetyp} eq "13") {
189 if ($form->{duetyp} eq "A") {
191 $days = 90 + $leapday;
193 if ($form->{duetyp} eq "B") {
199 if ($form->{duetyp} eq "C") {
205 if ($form->{duetyp} eq "D") {
210 if ($form->{duetyp} eq "1" || $form->{duetyp} eq "3" || $form->{duetyp} eq "5" ||
211 $form->{duetyp} eq "7" || $form->{duetyp} eq "8" || $form->{duetyp} eq "10" ||
212 $form->{duetyp} eq "12") {
213 $smon = $emon = $form->{duetyp}*1;
216 if ($form->{duetyp} eq "2" || $form->{duetyp} eq "4" || $form->{duetyp} eq "6" ||
217 $form->{duetyp} eq "9" || $form->{duetyp} eq "11" ) {
218 $smon = $emon = $form->{duetyp}*1;
220 if ($form->{duetyp} eq "2" ) {
221 #this works from 1901 to 2099, 1900 and 2100 fail.
222 $eday = ($form->{year} % 4 == 0) ? 29 : 28;
224 $mdays=$days = $eday;
226 $searchparams{year} = $form->{year};
227 $searchparams{duetyp} = $form->{duetyp};
228 $start->set_month($smon);
229 $start->set_day($sday);
230 $start->set_year($form->{year}*1);
231 $end->set_month($emon);
232 $end->set_day($eday);
233 $end->set_year($form->{year}*1);
235 $searchparams{fromdate} = $form->{fromdate};
236 $searchparams{todate} = $form->{todate};
238 # fromdate = 01.01.2014
239 # todate = 31.05.2014
240 my ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{fromdate});
241 $start->set_year($yy);
242 $start->set_month($mm);
243 $start->set_day($dd);
244 ($yy, $mm, $dd) = $locale->parse_date(\%myconfig,$form->{todate});
246 $end->set_month($mm);
248 my $dur = $start->delta_md($end);
249 $days = $dur->delta_months()*30 + $dur->delta_days() ;
251 $start->set_second(0);
252 $start->set_minute(0);
254 $end->set_second(59);
255 $end->set_minute(59);
257 if ( $end->epoch() > $actualepoch ) {
258 $end = DateTime->now_local;
259 my $dur = $start->delta_md($end);
260 $days = $dur->delta_months()*30 + $dur->delta_days() ;
262 if ( $start->epoch() > $end->epoch() ) { $start = $end;$days = 1;}
263 $days = $mdays if $days < $mdays;
264 #$main::lxdebug->message(LXDebug->DEBUG2(), "start=".$start->epoch());
265 #$main::lxdebug->message(LXDebug->DEBUG2(), " end=".$end->epoch());
266 #$main::lxdebug->message(LXDebug->DEBUG2(), " days=".$days);
267 my @andfilter = (shippingdate => { ge => $start }, shippingdate => { le => $end } );
268 if ( $form->{warehouse_id} ) {
269 push @andfilter , ( warehouse_id => $form->{warehouse_id});
270 $searchparams{warehouse_id} = $form->{warehouse_id};
271 if ( $form->{bin_id} ) {
272 push @andfilter , ( bin_id => $form->{bin_id});
273 $searchparams{bin_id} = $form->{bin_id};
276 # alias class t2 entspricht parts
277 if ( $form->{partnumber} ) {
278 push @andfilter , ( 't2.partnumber' => { ilike => '%'. $form->{partnumber} .'%' });
279 $searchparams{partnumber} = $form->{partnumber};
281 if ( $form->{description} ) {
282 push @andfilter , ( 't2.description' => { ilike => '%'. $form->{description} .'%' });
283 $searchparams{description} = $form->{description};
285 if ( $form->{bestbefore} ) {
286 push @andfilter , ( bestbefore => { eq => $form->{bestbefore} });
287 $searchparams{bestbefore} = $form->{bestbefore};
289 if ( $form->{chargenumber} ) {
290 push @andfilter , ( chargenumber => { ilike => '%'.$form->{chargenumber}.'%' });
291 $searchparams{chargenumber} = $form->{chargenumber};
293 if ( $form->{partstypes_id} ) {
294 push @andfilter , ( 't2.partstypes_id' => $form->{partstypes_id} );
295 $searchparams{partstypes_id} = $form->{partstypes_id};
298 my @filter = (and => [ @andfilter ] );
300 my $objs = SL::DB::Manager::Inventory->get_all(with_objects => ['parts'], where => [ @filter ] , sort_by => 'parts.partnumber ASC');
301 #my $objs = SL::DB::Inventory->_get_manager_class->get_all(...);
303 # manual paginating, yuck
304 my $page = $::form->{page} || 1;
306 $pages->{per_page} = $::form->{per_page} || 20;
307 my $first_nr = ($page - 1) * $pages->{per_page};
308 my $last_nr = $first_nr + $pages->{per_page};
314 $allrows = 1 if $form->{report_generator_output_format} ne 'HTML' ;
315 #$main::lxdebug->message(LXDebug->DEBUG2(), "first_nr=".$first_nr." last_nr=".$last_nr);
316 foreach my $entry (@{ $objs } ) {
317 if ( $entry->parts_id != $last_partid ) {
318 if ( $last_partid > 0 ) {
319 if ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr )) {
320 $self->make_row_result($last_row,$days,$last_partid);
321 $report->add_data($last_row);
325 $last_partid = $entry->parts_id;
327 $last_row->{partnumber}->{data} = $entry->part->partnumber;
328 $last_row->{partdescription}->{data} = $entry->part->description;
329 $last_row->{unit}->{data} = $entry->part->unit;
330 $last_row->{stock}->{data} = 0;
331 $last_row->{incorrection}->{data} = 0;
332 $last_row->{found}->{data} = 0;
333 $last_row->{back}->{data} = 0;
334 $last_row->{outcorrection}->{data} = 0;
335 $last_row->{disposed}->{data} = 0;
336 $last_row->{missing}->{data} = 0;
337 $last_row->{shipped}->{data} = 0;
338 $last_row->{used}->{data} = 0;
339 $last_row->{insum}->{data} = 0;
340 $last_row->{outsum}->{data} = 0;
341 $last_row->{consumed}->{data} = 0;
342 $last_row->{averconsumed}->{data} = 0;
344 if ( !$allrows && $row_ind >= $last_nr ) {
348 if ( $entry->trans_type->description eq 'correction' ) {
349 $prefix = $entry->trans_type->direction;
351 $last_row->{$prefix.$entry->trans_type->description}->{data} +=
352 ( $entry->trans_type->direction eq 'out' ? -$entry->qty : $entry->qty );
354 if ( $last_partid > 0 && ( $allrows || ($row_ind >= $first_nr && $row_ind < $last_nr ))) {
355 $self->make_row_result($last_row,$days,$last_partid);
356 $report->add_data($last_row);
359 my $num_rows = @{ $report->{data} } ;
360 #$main::lxdebug->message(LXDebug->DEBUG2(), "count=".$row_ind." rows=".$num_rows);
363 $pages->{max} = SL::DB::Helper::Paginated::ceil($row_ind, $pages->{per_page}) || 1;
364 $pages->{page} = $page < 1 ? 1: $page > $pages->{max} ? $pages->{max}: $page;
365 $pages->{common} = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{page}, $pages->{max}) } ];
366 $self->{pages} = $pages;
367 $searchparams{action} = "usage";
368 $self->{base_url} = $self->url_for(\%searchparams );
369 #$main::lxdebug->message(LXDebug->DEBUG2(), "page=".$pages->{page}." url=".$self->{base_url});
371 $report->set_options('raw_bottom_info_text' => $self->render('inventory/report_bottom', { output => 0 }) );
373 $report->generate_with_headers();
375 $main::lxdebug->leave_sub();
379 sub make_row_result {
380 my ($self,$row,$days,$partid) = @_;
381 my $form = $main::form;
382 my $myconfig = \%main::myconfig;
384 $row->{insum}->{data} = $row->{stock}->{data} + $row->{incorrection}->{data} + $row->{found}->{data};
385 $row->{outsum}->{data} = $row->{back}->{data} + $row->{outcorrection}->{data} + $row->{disposed}->{data} +
386 $row->{missing}->{data} + $row->{shipped}->{data} + $row->{used}->{data};
387 $row->{consumed}->{data} = $row->{outsum}->{data} -
388 $row->{outcorrection}->{data} - $row->{incorrection}->{data};
389 $row->{averconsumed}->{data} = $row->{consumed}->{data}*30/$days ;
390 map { $row->{$_}->{data} = $form->format_amount($myconfig,$row->{$_}->{data},2); } $self->getnumcolumns();
391 $row->{partnumber}->{link} = 'controller.pl?action=Part/edit&part.id' . $partid;
398 my $qty = $::form->parse_amount(\%::myconfig, $::form->{qty});
400 $transfer_error = t8('Cannot stock without amount');
402 $transfer_error = t8('Cannot stock negative amounts');
405 $::form->throw_on_error(sub {
408 parts => $self->part,
409 dst_bin => $self->bin,
410 dst_wh => $self->warehouse,
413 transfer_type => 'stock',
414 chargenumber => $::form->{chargenumber},
415 bestbefore => $::form->{bestbefore},
416 ean => $::form->{ean},
417 comment => $::form->{comment},
420 } or do { $transfer_error = $EVAL_ERROR->getMessage; }
423 if (!$transfer_error) {
424 if ($::form->{write_default_bin}) {
425 $self->part->load; # onhand is calculated in between. don't mess that up
426 $self->part->bin($self->bin);
427 $self->part->warehouse($self->warehouse);
431 flash_later('info', t8('Transfer successful'));
435 my %additional_redirect_params = ();
436 if ($transfer_error) {
437 flash_later('error', $transfer_error);
438 $additional_redirect_params{$_} = $::form->{$_} for qw(qty chargenumber bestbefore ean comment);
439 $additional_redirect_params{qty} = $qty;
444 action => 'stock_in',
445 part_id => $self->part->id,
446 bin_id => $self->bin->id,
447 warehouse_id => $self->warehouse->id,
448 unit_id => $self->unit->id,
449 %additional_redirect_params,
453 sub action_part_changed {
456 # no standard? ask user if he wants to write it
457 if ($self->part->id && !$self->part->bin_id && !$self->part->warehouse_id) {
458 $self->js->show('#write_default_bin_span');
460 $self->js->hide('#write_default_bin_span')
461 ->removeAttr('#write_default_bin', 'checked');
465 ->replaceWith('#warehouse_id', $self->build_warehouse_select)
466 ->replaceWith('#bin_id', $self->build_bin_select)
467 ->replaceWith('#unit_id', $self->build_unit_select)
468 ->focus('#warehouse_id')
472 sub action_warehouse_changed {
476 ->replaceWith('#bin_id', $self->build_bin_select)
481 sub action_mini_stock {
485 ->html('#stock', $self->render('inventory/_stock', { output => 0 }))
489 sub action_stocktaking {
492 $::request->{layout}->use_javascript("${_}.js") for qw(kivi.Inventory);
493 $::request->layout->focus('#part_id_name');
494 $self->setup_stock_stocktaking_action_bar;
495 $self->render('inventory/stocktaking/form', title => t8('Stocktaking'));
498 sub action_save_stocktaking {
501 return $self->js->flash('error', t8('A target quantitiy has to be given'))->render()
502 if $::form->{target_qty} eq '';
504 my $target_qty = $::form->parse_amount(\%::myconfig, $::form->{target_qty});
506 return $self->js->flash('error', t8('Error: A negative target quantity is not allowed.'))->render()
509 my $stocked_qty = _get_stocked_qty($self->part,
510 warehouse_id => $self->warehouse->id,
511 bin_id => $self->bin->id,
512 chargenumber => $::form->{chargenumber},
513 bestbefore => $::form->{bestbefore},);
515 my $stocked_qty_in_form_units = $self->part->unit_obj->convert_to($stocked_qty, $self->unit);
517 if (!$::form->{dont_check_already_counted}) {
518 my $already_counted = _already_counted($self->part,
519 warehouse_id => $self->warehouse->id,
520 bin_id => $self->bin->id,
521 cutoff_date => $::form->{cutoff_date_as_date},
522 chargenumber => $::form->{chargenumber},
523 bestbefore => $::form->{bestbefore});
524 if (scalar @$already_counted) {
525 my $reply = $self->js->dialog->open({
526 html => $self->render('inventory/stocktaking/_already_counted_dialog',
528 already_counted => $already_counted,
529 stocked_qty => $stocked_qty,
530 stocked_qty_in_form_units => $stocked_qty_in_form_units),
531 id => 'already_counted_dialog',
533 title => t8('Already counted'),
541 # - target_qty is in units given in form ($self->unit)
542 # - WH->transfer expects qtys in given unit (here: unit from form (unit -> $self->unit))
543 # Therefore use stocked_qty in form units for calculation.
544 my $qty = $target_qty - $stocked_qty_in_form_units;
545 my $src_or_dst = $qty < 0? 'src' : 'dst';
550 $::form->throw_on_error(sub {
553 parts => $self->part,
554 $src_or_dst.'_bin' => $self->bin,
555 $src_or_dst.'_wh' => $self->warehouse,
558 transfer_type => 'stocktaking',
559 chargenumber => $::form->{chargenumber},
560 bestbefore => $::form->{bestbefore},
561 ean => $::form->{ean},
562 comment => $::form->{comment},
563 record_stocktaking => 1,
564 stocktaking_qty => $target_qty,
565 stocktaking_cutoff_date => $::form->{cutoff_date_as_date},
568 } or do { $transfer_error = $EVAL_ERROR->getMessage; }
571 return $self->js->flash('error', $transfer_error)->render()
574 flash_later('info', $::locale->text('Part successful counted'));
575 $self->redirect_to(action => 'stocktaking',
576 warehouse_id => $self->warehouse->id,
577 bin_id => $self->bin->id,
578 cutoff_date_as_date => $self->stocktaking_cutoff_date->to_kivitendo);
581 sub action_reload_stocktaking_history {
584 $::form->{filter}{'cutoff_date:date'} = $self->stocktaking_cutoff_date->to_kivitendo;
585 $::form->{filter}{'employee_id'} = SL::DB::Manager::Employee->current->id;
587 $self->prepare_stocktaking_report;
588 $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get, layout => 0, header => 0);
591 sub action_stocktaking_part_changed {
595 ->replaceWith('#unit_id', $self->build_unit_select)
596 ->focus('#target_qty')
600 sub action_stocktaking_journal {
603 $self->prepare_stocktaking_report(full => 1);
604 $self->report_generator_list_objects(report => $self->{report}, objects => $self->stocktaking_models->get);
606 #================================================================
609 $main::auth->assert('warehouse_management');
612 sub _check_warehouses {
613 $_[0]->show_no_warehouses_error if !@{ $_[0]->warehouses };
616 sub init_warehouses {
617 SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
621 # SL::DB::Manager::Bin->get_all();
625 SL::DB::Manager::Unit->get_all;
628 sub init_is_stocktaking {
629 return $_[0]->action_name =~ m{stocktaking};
632 sub init_stocktaking_models {
635 SL::Controller::Helper::GetModels->new(
637 model => 'Stocktaking',
643 itime => t8('Insert Date'),
644 qty => t8('Target Qty'),
645 chargenumber => t8('Charge Number'),
646 comment => t8('Comment'),
647 employee => t8('Employee'),
649 partnumber => t8('Part Number'),
650 part => t8('Part Description'),
652 cutoff_date => t8('Cutoff Date'),
654 with_objects => ['employee', 'parts', 'warehouse', 'bin'],
658 sub init_stocktaking_cutoff_date {
661 return DateTime->from_kivitendo($::form->{cutoff_date_as_date}) if $::form->{cutoff_date_as_date};
662 return SL::DB::Default->get->stocktaking_cutoff_date if SL::DB::Default->get->stocktaking_cutoff_date;
664 # Default cutoff date is last day of current year, but if current month
665 # is janurary, it is the last day of the last year.
666 my $now = DateTime->now_local;
667 my $cutoff = DateTime->new(year => $now->year, month => 12, day => 31);
668 if ($now->month < 1) {
669 $cutoff->substract(years => 1);
674 sub set_target_from_part {
677 return if !$self->part;
679 $self->warehouse($self->part->warehouse) if $self->part->warehouse;
680 $self->bin( $self->part->bin) if $self->part->bin;
683 sub sanitize_target {
686 $self->warehouse($self->warehouses->[0]) if !$self->warehouse || !$self->warehouse->id;
687 $self->bin ($self->warehouse->bins->[0]) if !$self->bin || !$self->bin->id;
688 # foreach my $warehouse ( $self->warehouses ) {
689 # $warehouse->{BINS} = [];
690 # foreach my $bin ( $self->bins ) {
691 # if ( $bin->warehouse_id == $warehouse->id ) {
692 # push @{ $warehouse->{BINS} }, $bin;
698 sub load_part_from_form {
699 $_[0]->part(SL::DB::Manager::Part->find_by_or_create(id => $::form->{part_id}));
702 sub load_unit_from_form {
703 $_[0]->unit(SL::DB::Manager::Unit->find_by_or_create(id => $::form->{unit_id}));
706 sub load_wh_from_form {
708 $preselected = SL::DB::Default->get->stocktaking_warehouse_id if $_[0]->is_stocktaking;
710 $_[0]->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => ($::form->{warehouse_id} || $preselected)));
713 sub load_bin_from_form {
715 $preselected = SL::DB::Default->get->stocktaking_bin_id if $_[0]->is_stocktaking;
717 $_[0]->bin(SL::DB::Manager::Bin->find_by_or_create(id => ($::form->{bin_id} || $preselected)));
721 $::request->layout->add_javascripts('client_js.js');
724 sub build_warehouse_select {
725 select_tag('warehouse_id', $_[0]->warehouses,
726 title_key => 'description',
727 default => $_[0]->warehouse->id,
728 onchange => 'reload_bin_selection()',
732 sub build_bin_select {
733 select_tag('bin_id', [ $_[0]->warehouse->bins ],
734 title_key => 'description',
735 default => $_[0]->bin->id,
739 sub build_unit_select {
741 ? select_tag('unit_id', $_[0]->part->available_units,
743 default => $_[0]->part->unit_obj->id,
745 : select_tag('unit_id', $_[0]->units,
753 # get last 10 transaction ids
754 my $query = 'SELECT trans_id, max(itime) FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';
755 my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query);
758 $objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
760 # at most 2 of them belong to a transaction and the qty determins in or out.
761 # sort them for display
764 $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
765 $transactions{ $_->trans_id }{base} = $_;
767 # and get them into order again
768 my @sorted = map { $transactions{$_} } @ids;
776 my $stock = $self->part->get_simple_stock;
777 $self->{stock_by_bin} = { map { $_->{bin_id} => $_ } @$stock };
778 $self->{stock_empty} = ! grep { $_->{sum} * 1 } @$stock;
781 sub show_no_warehouses_error {
784 my $msg = t8('No warehouse has been created yet or the quantity of the bins is not configured yet.') . ' ';
786 if ($::auth->check_right($::myconfig{login}, 'config')) { # TODO wut?
787 $msg .= t8('You can create warehouses and bins via the menu "System -> Warehouses".');
789 $msg .= t8('Please ask your administrator to create warehouses and bins.');
791 $::form->show_generic_error($msg);
794 sub prepare_stocktaking_report {
795 my ($self, %params) = @_;
797 my $callback = $self->stocktaking_models->get_callback;
799 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
800 $self->{report} = $report;
802 my @columns = qw(itime employee ean partnumber part qty unit bin chargenumber comment cutoff_date);
803 my @sortable = qw(itime employee ean partnumber part qty bin chargenumber comment cutoff_date);
806 itime => { sub => sub { $_[0]->itime_as_timestamp },
807 text => t8('Insert Date'), },
808 employee => { sub => sub { $_[0]->employee->safe_name },
809 text => t8('Employee'), },
810 ean => { sub => sub { $_[0]->part->ean },
811 text => t8('EAN'), },
812 partnumber => { sub => sub { $_[0]->part->partnumber },
813 text => t8('Part Number'), },
814 part => { sub => sub { $_[0]->part->description },
815 text => t8('Part Description'), },
816 qty => { sub => sub { $_[0]->qty_as_number },
817 text => t8('Target Qty'),
819 unit => { sub => sub { $_[0]->part->unit },
820 text => t8('Unit'), },
821 bin => { sub => sub { $_[0]->bin->full_description },
822 text => t8('Bin'), },
823 chargenumber => { text => t8('Charge Number'), },
824 comment => { text => t8('Comment'), },
825 cutoff_date => { sub => sub { $_[0]->cutoff_date_as_date },
826 text => t8('Cutoff Date'), },
829 $report->set_options(
830 std_column_visibility => 1,
831 controller_class => 'Inventory',
832 output_format => 'HTML',
833 title => (!!$params{full})? $::locale->text('Stocktaking Journal') : $::locale->text('Stocktaking History'),
834 allow_pdf_export => !!$params{full},
835 allow_csv_export => !!$params{full},
837 $report->set_columns(%column_defs);
838 $report->set_column_order(@columns);
839 $report->set_export_options(qw(stocktaking_journal filter));
840 $report->set_options_from_form;
841 $self->stocktaking_models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
842 $self->stocktaking_models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable) if !!$params{full};
843 if (!!$params{full}) {
844 $report->set_options(
845 raw_top_info_text => $self->render('inventory/stocktaking/full_report_top', { output => 0 }),
848 $report->set_options(
849 raw_bottom_info_text => $self->render('inventory/stocktaking/report_bottom', { output => 0 }),
853 sub _get_stocked_qty {
854 my ($part, %params) = @_;
856 my $bestbefore_filter = '';
857 my $bestbefore_val_cnt = 0;
858 if ($::instance_conf->get_show_bestbefore) {
859 $bestbefore_filter = ($params{bestbefore}) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
860 $bestbefore_val_cnt = ($params{bestbefore}) ? 1 : 0;
864 SELECT sum(qty) FROM inventory
865 WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
866 GROUP BY warehouse_id, bin_id, chargenumber
869 my @values = ($part->id,
870 $params{warehouse_id},
872 $params{chargenumber});
873 push @values, $params{bestbefore} if $bestbefore_val_cnt;
875 my ($stocked_qty) = selectrow_query($::form, $::form->get_standard_dbh, $query, @values);
877 return 1*($stocked_qty || 0);
880 sub _already_counted {
881 my ($part, %params) = @_;
883 my %bestbefore_filter;
884 if ($::instance_conf->get_show_bestbefore) {
885 %bestbefore_filter = (bestbefore => $params{bestbefore});
888 SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id => $part->id,
889 warehouse_id => $params{warehouse_id},
890 bin_id => $params{bin_id},
891 cutoff_date => $params{cutoff_date},
892 chargenumber => $params{chargenumber},
893 %bestbefore_filter]],
894 sort_by => ['itime DESC']);
897 sub setup_stock_in_action_bar {
898 my ($self, %params) = @_;
900 for my $bar ($::request->layout->get('actionbar')) {
904 submit => [ '#form', { action => 'Inventory/stock' } ],
905 checks => [ 'check_part_selection_before_stocking' ],
906 accesskey => 'enter',
912 sub setup_stock_usage_action_bar {
913 my ($self, %params) = @_;
915 for my $bar ($::request->layout->get('actionbar')) {
919 submit => [ '#form', { action => 'Inventory/usage' } ],
920 accesskey => 'enter',
926 sub setup_stock_stocktaking_action_bar {
927 my ($self, %params) = @_;
929 for my $bar ($::request->layout->get('actionbar')) {
933 call => [ 'kivi.Inventory.save_stocktaking' ],
934 accesskey => 'enter',
947 SL::Controller::Inventory - Controller for inventory
951 This controller handles stock in, stocktaking and reports about inventory
958 - warehouse withdrawal
964 Stocktaking allows to document the counted quantities of parts during
965 stocktaking for a certain cutoff date. Differences between counted and stocked
966 quantities are corrected in the stock. The transfer type 'stocktacking' is set
969 After picking a part, the mini stock for this part is displayed. At the bottom
970 of the form a history of already counted parts for the current employee and the
971 choosen cutoff date is shown.
973 Warehouse, bin and cutoff date canbe preselected in the client configuration.
975 If a part was already counted for this cutoff date, warehouse and bin, a warning
976 is displayed, allowing the user to choose to add the counted quantity to the
977 stocked one or to take his counted quantity as the new stocked quantity.
979 There is also a journal of stocktakings.
981 Templates are located under C<templates/webpages/inventory/stocktaking>.
982 JavaScript functions can be found in C<js/kivi.Inventory.js>.
988 =item C<action_stock_usage>
990 Create a search form for stock withdrawal.
991 The search parameter for report are made like the reports in bin/mozilla/rp.pl
993 =item C<action_usage>
995 Make a report about stock withdrawal.
997 The manual pagination is implemented like the pagination in SL::Controller::CsvImport.
999 =item C<action_stocktaking>
1001 This action renders the input form for stocktaking.
1003 =item C<action_save_stocktaking>
1005 This action saves the stocktaking values and corrects the stock after checking
1006 if the part is already counted for this warehouse, bin and cutoff date.
1007 For saving SL::WH->transfer is called.
1009 =item C<action_reload_stocktaking_history>
1011 This action is responsible for displaying the stocktaking history at the bottom
1012 of the form. It uses the stocktaking journal with fixed filters for cutoff date
1013 and the current employee. The history is displayed via javascript.
1015 =item C<action_stocktaking_part_changed>
1017 This action is called after the user selected or changed the part.
1019 =item C<is_stocktaking>
1021 This is a method to check if actions are called from stocktaking form.
1025 =head1 SPECIAL CASES
1027 Because of the PFD-Table Formatter some parameters for PDF must be different to the HTML parameters.
1028 So in german language there are some tries to use a HTML Break in the second heading line
1029 to produce two line heading inside table. The actual version has some abbreviations for the header texts.
1033 The PDF-Table library has some limits (doesn't display all if the line is to large) so
1034 the format is adapted to this
1041 =item only for C<action_stock_usage> and C<action_usage>:
1043 Martin Helmling E<lt>martin.helmling@opendynamic.deE<gt>
1045 =item for stocktaking:
1047 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>