1 #=====================================================================
 
   4 # Based on SQL-Ledger Version 2.1.9
 
   5 # Web http://www.lx-office.org
 
   7 #=====================================================================
 
   8 # SQL-Ledger, Accounting
 
  11 #  Author: Dieter Simader
 
  12 #   Email: dsimader@sql-ledger.org
 
  13 #     Web: http://www.sql-ledger.org
 
  16 # This program is free software; you can redistribute it and/or modify
 
  17 # it under the terms of the GNU General Public License as published by
 
  18 # the Free Software Foundation; either version 2 of the License, or
 
  19 # (at your option) any later version.
 
  21 # This program is distributed in the hope that it will be useful,
 
  22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  24 # GNU General Public License for more details.
 
  25 # You should have received a copy of the GNU General Public License
 
  26 # along with this program; if not, write to the Free Software
 
  27 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
  29 #======================================================================
 
  31 # Inventory Control module
 
  33 #======================================================================
 
  35 use POSIX qw(strftime);
 
  36 use List::Util qw(first max);
 
  37 use List::MoreUtils qw(any);
 
  42 use SL::Helper::Flash qw(flash);
 
  44 use SL::ReportGenerator;
 
  52 our ($form, $locale, %myconfig, $lxdebug, $auth);
 
  54 require "bin/mozilla/io.pl";
 
  55 require "bin/mozilla/common.pl";
 
  56 require "bin/mozilla/reportgenerator.pl";
 
  61 # type=submit $locale->text('Add Part')
 
  62 # type=submit $locale->text('Add Service')
 
  63 # type=submit $locale->text('Add Assembly')
 
  64 # type=submit $locale->text('Edit Part')
 
  65 # type=submit $locale->text('Edit Service')
 
  66 # type=submit $locale->text('Edit Assembly')
 
  67 # $locale->text('Parts')
 
  68 # $locale->text('Services')
 
  69 # $locale->text('Inventory quantity must be zero before you can set this part obsolete!')
 
  70 # $locale->text('Inventory quantity must be zero before you can set this assembly obsolete!')
 
  71 # $locale->text('Part Number missing!')
 
  72 # $locale->text('Service Number missing!')
 
  73 # $locale->text('Assembly Number missing!')
 
  74 # $locale->text('ea');
 
  79   $lxdebug->enter_sub();
 
  81   $auth->assert('part_service_assembly_details');
 
  83   $form->{revers}       = 0;  # switch for backward sorting
 
  84   $form->{lastsort}     = ""; # memory for which table was sort at last time
 
  85   $form->{ndxs_counter} = 0;  # counter for added entries to top100
 
  87   my %is_xyz     = map { +"is_$_" => ($form->{searchitems} eq $_) } qw(part service assembly);
 
  89   $form->{title} = (ucfirst $form->{searchitems}) . "s";
 
  90   $form->{title} = $locale->text($form->{title});
 
  91   $form->{title} = $locale->text('Assemblies') if ($is_xyz{is_assembly});
 
  93   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
 
  94   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
 
  95    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
 
  96                                                                            'include_prefix' => 'l_',
 
  97                                                                            'include_value'  => 'Y');
 
 101   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
 
 102   print $form->parse_html_template('ic/search', { %is_xyz, });
 
 104   $lxdebug->leave_sub();
 
 107 sub search_update_prices {
 
 108   $lxdebug->enter_sub();
 
 110   $auth->assert('part_service_assembly_edit');
 
 112   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
 
 114   $form->{title} = $locale->text('Update Prices');
 
 118   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
 
 120   $lxdebug->leave_sub();
 
 123 sub confirm_price_update {
 
 124   $lxdebug->enter_sub();
 
 126   $auth->assert('part_service_assembly_edit');
 
 129   my $value_found = undef;
 
 131   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
 
 132     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
 
 133     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
 
 134     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
 
 135     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
 
 137     if ((0 > $value) && ($type eq 'percent')) {
 
 138       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
 
 140     } elsif (!$value && ($form->{$value_idx} ne '')) {
 
 141       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
 
 143     } elsif (0 < $value) {
 
 148   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
 
 150   my $num_matches = IC->get_num_matches_for_priceupdate();
 
 155     $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
 
 158   $form->{nextsub} = "update_prices";
 
 160   map { delete $form->{$_} } qw(action header);
 
 162   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
 
 163                                                                 num_matches => $num_matches });
 
 165   $lxdebug->leave_sub();
 
 169   $lxdebug->enter_sub();
 
 171   $auth->assert('part_service_assembly_edit');
 
 173   my $num_updated = IC->update_prices(\%myconfig, \%$form);
 
 175   if (-1 != $num_updated) {
 
 176     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
 
 178     $form->error($locale->text('Could not update prices!'));
 
 181   $lxdebug->leave_sub();
 
 185   $::lxdebug->enter_sub();
 
 187   $::auth->assert('part_service_assembly_edit');
 
 189   $::form->{l_soldtotal} = "Y";
 
 190   $::form->{sort}        = "soldtotal";
 
 191   $::form->{lastsort}    = "soldtotal";
 
 193   $::form->{l_qty}       = undef;
 
 194   $::form->{l_linetotal} = undef;
 
 195   $::form->{l_number}    = "Y";
 
 196   $::form->{number}      = "position";
 
 198   unless (   $::form->{bought}
 
 201           || $::form->{quoted}) {
 
 202     $::form->{bought} = $::form->{sold} = 1;
 
 207   $lxdebug->leave_sub();
 
 212 # Warning, deep magic ahead.
 
 213 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
 
 215 # flags coming from the form:
 
 217 #  searchitems=part revers=0 lastsort=''
 
 220 # partnumber ean description partsgroup serialnumber make model drawing microfiche
 
 221 # transdatefrom transdateto
 
 224 #  itemstatus = active | onhand | short | obsolete | orphaned
 
 225 #  action     = continue | top100
 
 228 #  bought sold onorder ordered rfq quoted
 
 229 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
 
 230 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
 
 231 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
 
 234 #  nextsub revers lastsort sort ndxs_counter
 
 236 sub generate_report {
 
 237   $lxdebug->enter_sub();
 
 239   $auth->assert('part_service_assembly_details');
 
 241   my ($revers, $lastsort, $description);
 
 243   my $cvar_configs = CVar->get_configs('module' => 'IC');
 
 246     ''         => $locale->text('Articles'),
 
 247     part       => $locale->text('Parts'),
 
 248     service    => $locale->text('Services'),
 
 249     assembly   => $locale->text('Assemblies'),
 
 250     assortment => $locale->text('Assortments'),
 
 253   $form->{title} = $titles{$form->{searchitems}};
 
 256     'bin'                => { 'text' => $locale->text('Bin'), },
 
 257     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
 
 258     'description'        => { 'text' => $locale->text('Part Description'), },
 
 259     'notes'              => { 'text' => $locale->text('Notes'), },
 
 260     'drawing'            => { 'text' => $locale->text('Drawing'), },
 
 261     'ean'                => { 'text' => $locale->text('EAN'), },
 
 262     'image'              => { 'text' => $locale->text('Image'), },
 
 263     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
 
 264     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
 
 265     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
 
 266     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
 
 267     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
 
 268     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
 
 269     'listprice'          => { 'text' => $locale->text('List Price'), },
 
 270     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
 
 271     'name'               => { 'text' => $locale->text('Name'), },
 
 272     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
 
 273     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
 
 274     'partnumber'         => { 'text' => $locale->text('Part Number'), },
 
 275     'partsgroup'         => { 'text' => $locale->text('Group'), },
 
 276     'priceupdate'        => { 'text' => $locale->text('Updated'), },
 
 277     'quonumber'          => { 'text' => $locale->text('Quotation'), },
 
 278     'rop'                => { 'text' => $locale->text('ROP'), },
 
 279     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
 
 280     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
 
 281     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
 
 282     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
 
 283     'transdate'          => { 'text' => $locale->text('Transdate'), },
 
 284     'unit'               => { 'text' => $locale->text('Unit'), },
 
 285     'weight'             => { 'text' => $locale->text('Weight'), },
 
 286     'shop'               => { 'text' => $locale->text('Shop article'), },
 
 287     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
 
 288     'projectdescription' => { 'text' => $locale->text('Project Description'), },
 
 291   $revers     = $form->{revers};
 
 292   $lastsort   = $form->{lastsort};
 
 294   # sorting and direction of sorting
 
 295   # ToDO: change this to the simpler field+direction method
 
 296   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
 
 298     $form->{lastsort} = "partnumber";
 
 299     $form->{sort}     = "partnumber";
 
 301     if ($form->{lastsort} eq $form->{sort}) {
 
 302       $form->{revers} = 1 - $form->{revers};
 
 305       $form->{lastsort} = $form->{sort};
 
 309   # special case if we have a serialnumber limit search
 
 310   # serialnumbers are only given in invoices and orders,
 
 311   # so they can only pop up in bought, sold, rfq, and quoted stuff
 
 312   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
 
 313                                  && !$form->{rfq}    && !$form->{quoted}
 
 314                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
 
 316   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
 
 317   # if any of these are ticked the behavior changes slightly for lastcost
 
 318   # since all those are aggregation checks for the legder tables this is an internal switch
 
 319   # refered to as ledgerchecks
 
 320   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
 
 321                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
 
 323   # if something should be activated if something else is active, enter it here
 
 325     onhand       => [ qw(l_onhand) ],
 
 326     short        => [ qw(l_onhand) ],
 
 327     onorder      => [ qw(l_ordnumber) ],
 
 328     ordered      => [ qw(l_ordnumber) ],
 
 329     rfq          => [ qw(l_quonumber) ],
 
 330     quoted       => [ qw(l_quonumber) ],
 
 331     bought       => [ qw(l_invnumber) ],
 
 332     sold         => [ qw(l_invnumber) ],
 
 333     ledgerchecks => [ qw(l_name) ],
 
 334     serialnumber => [ qw(l_serialnumber) ],
 
 335     no_sn_joins  => [ qw(bought sold) ],
 
 338   # get name of partsgroup if id is given
 
 340   if ($form->{partsgroup_id}) {
 
 341     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
 
 342     $pg_name = $pg->{'partsgroup'};
 
 345   # these strings get displayed at the top of the results to indicate the user which switches were used
 
 347     active        => $locale->text('Active'),
 
 348     obsolete      => $locale->text('Obsolete'),
 
 349     orphaned      => $locale->text('Orphaned'),
 
 350     onhand        => $locale->text('On Hand'),
 
 351     short         => $locale->text('Short'),
 
 352     onorder       => $locale->text('On Order'),
 
 353     ordered       => $locale->text('Ordered'),
 
 354     rfq           => $locale->text('RFQ'),
 
 355     quoted        => $locale->text('Quoted'),
 
 356     bought        => $locale->text('Bought'),
 
 357     sold          => $locale->text('Sold'),
 
 358     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
 
 359     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
 
 360     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
 
 361     partsgroup    => $locale->text('Group')            . ": '$form->{partsgroup}'",
 
 362     partsgroup_id => $locale->text('Group')            . ": '$pg_name'",
 
 363     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
 
 364     description   => $locale->text('Part Description') . ": '$form->{description}'",
 
 365     make          => $locale->text('Make')             . ": '$form->{make}'",
 
 366     model         => $locale->text('Model')            . ": '$form->{model}'",
 
 367     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
 
 368     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
 
 369     l_soldtotal   => $locale->text('Qty in Selected Records'),
 
 370     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
 
 371     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
 
 372     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
 
 375   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
 
 376   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
 
 377                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all);
 
 379   # calculate dependencies
 
 380   for (@itemstatus_keys, @callback_keys) {
 
 381     next if ($form->{itemstatus} ne $_ && !$form->{$_});
 
 382     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
 
 385   # generate callback and optionstrings
 
 387   for my  $key (@itemstatus_keys, @callback_keys) {
 
 388     next if ($form->{itemstatus} ne $key && !$form->{$key});
 
 389     push @options, $optiontexts{$key};
 
 392   # special case for lastcost
 
 393   if ($form->{ledgerchecks}){
 
 394     # ledgerchecks don't know about sellprice or lastcost. they just return a
 
 395     # price. so rename sellprice to price, and drop lastcost.
 
 396     $column_defs{sellprice}{text} = $locale->text('Price');
 
 397     $form->{l_lastcost} = ""
 
 400   if ($form->{description}) {
 
 401     $description = $form->{description};
 
 402     $description =~ s/\n/<br>/g;
 
 405   if ($form->{l_linetotal}) {
 
 406     $form->{l_qty} = "Y";
 
 407     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
 
 408     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
 
 409     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
 
 412   if ($form->{searchitems} eq 'service') {
 
 414     # remove bin, weight and rop from list
 
 415     map { $form->{"l_$_"} = "" } qw(bin weight rop);
 
 417     $form->{l_onhand} = "";
 
 419     # qty is irrelevant unless bought or sold
 
 425         || $form->{quoted}) {
 
 426 #      $form->{l_onhand} = "Y";
 
 428       $form->{l_linetotalsellprice} = "";
 
 429       $form->{l_linetotallastcost}  = "";
 
 433   # soldtotal doesn't make sense with more than one bsooqr option.
 
 434   # so reset it to sold (the most common option), and issue a warning
 
 436   # also it doesn't make sense without bsooqr. disable and issue a warning too
 
 437   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
 
 438   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
 
 439   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
 
 440     my $enabled       = first { $form->{$_} } @bsooqr;
 
 441     $form->{$_}       = ''   for @bsooqr;
 
 442     $form->{$enabled} = 'Y';
 
 444     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
 
 446   if ($form->{l_soldtotal} && !$bsooqr_mode) {
 
 447     delete $form->{l_soldtotal};
 
 449     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
 
 451   if ($form->{l_name} && !$bsooqr_mode) {
 
 452     delete $form->{l_name};
 
 454     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
 
 456   IC->all_parts(\%myconfig, \%$form);
 
 459     partnumber description notes partsgroup bin onhand rop soldtotal unit listprice
 
 460     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
 
 461     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
 
 462     transdate name serialnumber deliverydate ean projectnumber projectdescription
 
 466   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
 
 467   my @pricegroup_columns;
 
 468   my %column_defs_pricegroups;
 
 469   if ($form->{l_pricegroups}) {
 
 470     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
 
 471     %column_defs_pricegroups = map {
 
 472       "pricegroup_" . $_->id => {
 
 473         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
 
 478   push @columns, @pricegroup_columns;
 
 480   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
 
 481   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
 
 482   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
 
 484   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
 
 486   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
 
 487   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
 
 488   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
 
 490   my @hidden_variables = (
 
 491     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
 
 494     map({ "cvar_$_->{name}" } @searchable_custom_variables),
 
 495     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
 
 496     map({ "l_$_" } @columns),
 
 499   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
 
 501   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
 
 502   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
 
 504   foreach my $col (@sort_full) {
 
 505     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
 
 507   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
 
 509   # add order to callback
 
 510   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
 
 512   my $report = SL::ReportGenerator->new(\%myconfig, $form);
 
 514   my %attachment_basenames = (
 
 515     'part'     => $locale->text('part_list'),
 
 516     'service'  => $locale->text('service_list'),
 
 517     'assembly' => $locale->text('assembly_list'),
 
 520   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
 
 521                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom'),
 
 522                        'output_format'         => 'HTML',
 
 523                        'title'                 => $form->{title},
 
 524                        'attachment_basename'   => $attachment_basenames{$form->{searchitems}} . strftime('_%Y%m%d', localtime time),
 
 526   $report->set_options_from_form();
 
 527   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
 
 529   $report->set_columns(%column_defs);
 
 530   $report->set_column_order(@columns);
 
 532   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
 
 534   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
 
 536   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 537                                        'trans_id_field' => 'id',
 
 538                                        'configs'        => $cvar_configs,
 
 539                                        'column_defs'    => \%column_defs,
 
 540                                        'data'           => $form->{parts});
 
 542   CVar->add_custom_variables_to_report('module'         => 'IC',
 
 543                                        'sub_module'     => sub { $_[0]->{ioi} },
 
 544                                        'trans_id_field' => 'ioi_id',
 
 545                                        'configs'        => $cvar_configs,
 
 546                                        'column_defs'    => \%column_defs,
 
 547                                        'data'           => $form->{parts});
 
 549   my @subtotal_columns = qw(sellprice listprice lastcost);
 
 550   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
 
 551   my %totals    = map { $_ => 0 } @subtotal_columns;
 
 553   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
 
 555   my $defaults  = AM->get_defaults();
 
 558   foreach my $ref (@{ $form->{parts} }) {
 
 560     # fresh row, for inserting later
 
 561     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
 
 563     $ref->{exchangerate} ||= 1;
 
 564     $ref->{price_factor} ||= 1;
 
 565     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 566     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
 
 567     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
 
 569     # use this for assemblies
 
 570     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
 
 572     if ($ref->{assemblyitem}) {
 
 573       $row->{partnumber}{align}   = 'right';
 
 574       $row->{soldtotal}{data}     = 0;
 
 575       $soldtotal                  = 0 if ($form->{sold});
 
 578     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}), 'callback');
 
 579     $row->{partnumber}->{link}  = $edit_link;
 
 580     $row->{description}->{link} = $edit_link;
 
 582     foreach (qw(sellprice listprice lastcost)) {
 
 583       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
 
 584       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
 
 586     foreach ( @pricegroup_columns ) {
 
 587       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
 
 591     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
 
 593     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
 
 595     # 'yes' and 'no' for boolean value shop
 
 596     if ($form->{l_shop}) {
 
 597       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
 
 600     if (!$ref->{assemblyitem}) {
 
 601       foreach my $col (@subtotal_columns) {
 
 602         $totals{$col}    += $soldtotal * $ref->{$col};
 
 603         $subtotals{$col} += $soldtotal * $ref->{$col};
 
 606       $subtotals{soldtotal} += $soldtotal;
 
 610     if ($ref->{module} eq 'oe') {
 
 611       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
 
 613       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
 
 614       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
 
 616       my $edit_oe_ord_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'), 'id=' . E($ref->{trans_id}), 'callback');
 
 617       my $edit_oe_quo_link = build_std_url("script=oe.pl", 'action=edit', 'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
 
 619       $row->{ordnumber}{link} = $edit_oe_ord_link;
 
 620       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
 
 623       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
 
 626     # set properties of images
 
 627     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
 
 628       $row->{image}{data}     = '';
 
 629       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
 
 631     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
 
 633     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
 
 635     $report->add_data($row);
 
 637     my $next_ref = $form->{parts}[$idx + 1];
 
 639     # insert subtotal rows
 
 640     if (($form->{l_subtotal} eq 'Y') &&
 
 642          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
 
 643       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
 
 645       if (($form->{searchitems} ne 'assembly') || !$form->{bom}) {
 
 646         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
 
 649       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
 
 650       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
 
 652       $report->add_data($row);
 
 654       $same_item = $next_ref->{ $form->{sort} };
 
 660   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
 
 661     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
 
 663     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
 
 665     $report->add_separator();
 
 666     $report->add_data($row);
 
 669   $report->generate_with_headers();
 
 671   $lxdebug->leave_sub();
 
 672 }    #end generate_report
 
 674 sub ajax_autocomplete {
 
 675   $main::lxdebug->enter_sub();
 
 677   my $form     = $main::form;
 
 678   my %myconfig = %main::myconfig;
 
 680   $form->{column}          = 'description'     unless $form->{column} =~ /^partnumber|description$/;
 
 681   $form->{$form->{column}} = $form->{q}           || '';
 
 682   $form->{limit}           = ($form->{limit} * 1) || 10;
 
 683   $form->{searchitems}   ||= '';
 
 685   my @results = IC->all_parts(\%myconfig, $form);
 
 687   print $form->ajax_response_header(),
 
 688         $form->parse_html_template('ic/ajax_autocomplete');
 
 690   $main::lxdebug->leave_sub();
 
 697   delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
 
 699   $::auth->restore_form_from_session($::form->{previousform}, clobber => 1);
 
 700   $::form->{rowcount}--;
 
 701   $::form->{action}   = 'display_form';
 
 702   $::form->{callback} = $::form->{script} . '?' . join('&', map { $::form->escape($_) . '=' . $::form->escape($::form->{$_}) } sort keys %{ $::form });
 
 706 sub continue { call_sub($form->{"nextsub"}); }
 
 709   my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
 
 710   $::form->error($::locale->text('No action defined.')) unless $action;
 
 712   $::form->{dispatched_action} = $action;