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>