Merge branch 'f-chart-picker-in-gl'
[kivitendo-erp.git] / bin / mozilla / ic.pl
1 #=====================================================================
2 # LX-Office ERP
3 # Copyright (C) 2004
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
6 #
7 #=====================================================================
8 # SQL-Ledger, Accounting
9 # Copyright (c) 2001
10 #
11 #  Author: Dieter Simader
12 #   Email: dsimader@sql-ledger.org
13 #     Web: http://www.sql-ledger.org
14 #
15 #
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.
20 #
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,
28 # MA 02110-1335, USA.
29 #======================================================================
30 #
31 # Inventory Control module
32 #
33 #======================================================================
34
35 use POSIX qw(strftime);
36 use List::Util qw(first max);
37 use List::MoreUtils qw(any);
38
39 use SL::AM;
40 use SL::CVar;
41 use SL::IC;
42 use SL::Helper::Flash qw(flash);
43 use SL::HTML::Util;
44 use SL::ReportGenerator;
45
46 #use SL::PE;
47
48 use strict;
49 #use warnings;
50
51 # global imports
52 our ($form, $locale, %myconfig, $lxdebug, $auth);
53
54 require "bin/mozilla/io.pl";
55 require "bin/mozilla/common.pl";
56 require "bin/mozilla/reportgenerator.pl";
57
58 1;
59
60 # Parserhappy(R):
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');
75
76 # end of main
77
78 sub search {
79   $lxdebug->enter_sub();
80
81   $auth->assert('part_service_assembly_details');
82
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
86
87   # for seach all possibibilities, is_service only used as UNLESS so == 0
88   my %is_xyz     = ("is_part" => 1, "is_service" => 0, "is_assembly" =>1 );
89
90   $form->{title} = (ucfirst $form->{searchitems}) . "s";
91   $form->{title} =~ s/ys$/ies/;
92   $form->{title} = $locale->text($form->{title});
93
94   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
95   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
96    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
97                                                                            'include_prefix' => 'l_',
98                                                                            'include_value'  => 'Y');
99
100   $form->header;
101
102   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
103   print $form->parse_html_template('ic/search', { %is_xyz, });
104
105   $lxdebug->leave_sub();
106 }    #end search()
107
108 sub search_update_prices {
109   $lxdebug->enter_sub();
110
111   $auth->assert('part_service_assembly_edit');
112
113   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
114
115   $form->{title} = $locale->text('Update Prices');
116
117   $form->header;
118
119   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
120
121   $lxdebug->leave_sub();
122 }    #end search()
123
124 sub confirm_price_update {
125   $lxdebug->enter_sub();
126
127   $auth->assert('part_service_assembly_edit');
128
129   my @errors      = ();
130   my $value_found = undef;
131
132   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
133     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
134     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
135     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
136     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
137
138     if ((0 > $value) && ($type eq 'percent')) {
139       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
140
141     } elsif (!$value && ($form->{$value_idx} ne '')) {
142       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
143
144     } elsif (0 < $value) {
145       $value_found = 1;
146     }
147   }
148
149   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
150
151   my $num_matches = IC->get_num_matches_for_priceupdate();
152
153   $form->header();
154
155   if (@errors) {
156     $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
157   }
158
159   $form->{nextsub} = "update_prices";
160
161   map { delete $form->{$_} } qw(action header);
162
163   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
164                                                                 num_matches => $num_matches });
165
166   $lxdebug->leave_sub();
167 }
168
169 sub update_prices {
170   $lxdebug->enter_sub();
171
172   $auth->assert('part_service_assembly_edit');
173
174   my $num_updated = IC->update_prices(\%myconfig, \%$form);
175
176   if (-1 != $num_updated) {
177     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
178   } else {
179     $form->error($locale->text('Could not update prices!'));
180   }
181
182   $lxdebug->leave_sub();
183 }
184
185 sub top100 {
186   $::lxdebug->enter_sub();
187
188   $::auth->assert('part_service_assembly_edit');
189
190   $::form->{l_soldtotal} = "Y";
191   $::form->{sort}        = "soldtotal";
192   $::form->{lastsort}    = "soldtotal";
193
194   $::form->{l_qty}       = undef;
195   $::form->{l_linetotal} = undef;
196   $::form->{l_number}    = "Y";
197   $::form->{number}      = "position";
198
199   unless (   $::form->{bought}
200           || $::form->{sold}
201           || $::form->{rfq}
202           || $::form->{quoted}) {
203     $::form->{bought} = $::form->{sold} = 1;
204   }
205
206   generate_report();
207
208   $lxdebug->leave_sub();
209 }
210
211 #
212 # Report for Wares.
213 # Warning, deep magic ahead.
214 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
215 #
216 # flags coming from the form:
217 # hardcoded:
218 #  searchitems=part revers=0 lastsort=''
219 #
220 # filter:
221 # partnumber ean description partsgroup classification serialnumber make model drawing microfiche
222 # transdatefrom transdateto
223 #
224 # radio:
225 #  itemstatus = active | onhand | short | obsolete | orphaned
226 #  action     = continue | top100
227 #
228 # checkboxes:
229 #  bought sold onorder ordered rfq quoted
230 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
231 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
232 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
233 #
234 # hiddens:
235 #  nextsub revers lastsort sort ndxs_counter
236 #
237 sub generate_report {
238   $lxdebug->enter_sub();
239
240   $auth->assert('part_service_assembly_details');
241
242   my ($revers, $lastsort, $description);
243
244   my $cvar_configs = CVar->get_configs('module' => 'IC');
245
246   $form->{title} = $locale->text('Articles');
247
248   my %column_defs = (
249     'bin'                => { 'text' => $locale->text('Bin'), },
250     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
251     'description'        => { 'text' => $locale->text('Part Description'), },
252     'notes'              => { 'text' => $locale->text('Notes'), },
253     'drawing'            => { 'text' => $locale->text('Drawing'), },
254     'ean'                => { 'text' => $locale->text('EAN'), },
255     'image'              => { 'text' => $locale->text('Image'), },
256     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
257     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
258     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
259     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
260     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
261     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
262     'listprice'          => { 'text' => $locale->text('List Price'), },
263     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
264     'name'               => { 'text' => $locale->text('Name'), },
265     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
266     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
267     'partnumber'         => { 'text' => $locale->text('Part Number'), },
268     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
269     'priceupdate'        => { 'text' => $locale->text('Updated'), },
270     'quonumber'          => { 'text' => $locale->text('Quotation'), },
271     'rop'                => { 'text' => $locale->text('ROP'), },
272     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
273     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
274     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
275     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
276     'transdate'          => { 'text' => $locale->text('Transdate'), },
277     'unit'               => { 'text' => $locale->text('Unit'), },
278     'weight'             => { 'text' => $locale->text('Weight'), },
279     'shop'               => { 'text' => $locale->text('Shop article'), },
280     'type_and_classific' => { 'text' => $locale->text('Type'), },
281     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
282     'projectdescription' => { 'text' => $locale->text('Project Description'), },
283   );
284
285   $revers     = $form->{revers};
286   $lastsort   = $form->{lastsort};
287
288   # sorting and direction of sorting
289   # ToDO: change this to the simpler field+direction method
290   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
291     $form->{revers}   = 0;
292     $form->{lastsort} = "partnumber";
293     $form->{sort}     = "partnumber";
294   } else {
295     if ($form->{lastsort} eq $form->{sort}) {
296       $form->{revers} = 1 - $form->{revers};
297     } else {
298       $form->{revers} = 0;
299       $form->{lastsort} = $form->{sort};
300     }    #fi
301   }    #fi
302
303   # special case if we have a serialnumber limit search
304   # serialnumbers are only given in invoices and orders,
305   # so they can only pop up in bought, sold, rfq, and quoted stuff
306   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
307                                  && !$form->{rfq}    && !$form->{quoted}
308                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
309
310   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
311   # if any of these are ticked the behavior changes slightly for lastcost
312   # since all those are aggregation checks for the legder tables this is an internal switch
313   # refered to as ledgerchecks
314   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
315                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
316
317   # if something should be activated if something else is active, enter it here
318   my %dependencies = (
319     onhand       => [ qw(l_onhand) ],
320     short        => [ qw(l_onhand) ],
321     onorder      => [ qw(l_ordnumber) ],
322     ordered      => [ qw(l_ordnumber) ],
323     rfq          => [ qw(l_quonumber) ],
324     quoted       => [ qw(l_quonumber) ],
325     bought       => [ qw(l_invnumber) ],
326     sold         => [ qw(l_invnumber) ],
327     ledgerchecks => [ qw(l_name) ],
328     serialnumber => [ qw(l_serialnumber) ],
329     no_sn_joins  => [ qw(bought sold) ],
330   );
331
332   # get name of partsgroup if id is given
333   my $pg_name;
334   if ($form->{partsgroup_id}) {
335     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
336     $pg_name = $pg->{'partsgroup'};
337   }
338
339   # these strings get displayed at the top of the results to indicate the user which switches were used
340   my %optiontexts = (
341     active        => $locale->text('Active'),
342     obsolete      => $locale->text('Obsolete'),
343     orphaned      => $locale->text('Orphaned'),
344     onhand        => $locale->text('On Hand'),
345     short         => $locale->text('Short'),
346     onorder       => $locale->text('On Order'),
347     ordered       => $locale->text('Ordered'),
348     rfq           => $locale->text('RFQ'),
349     quoted        => $locale->text('Quoted'),
350     bought        => $locale->text('Bought'),
351     sold          => $locale->text('Sold'),
352     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
353     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
354     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
355     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
356     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
357     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
358     description   => $locale->text('Part Description') . ": '$form->{description}'",
359     make          => $locale->text('Make')             . ": '$form->{make}'",
360     model         => $locale->text('Model')            . ": '$form->{model}'",
361     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
362     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
363     l_soldtotal   => $locale->text('Qty in Selected Records'),
364     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
365     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
366     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
367   );
368
369   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
370   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
371                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all);
372
373   # calculate dependencies
374   for (@itemstatus_keys, @callback_keys) {
375     next if ($form->{itemstatus} ne $_ && !$form->{$_});
376     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
377   }
378
379   # generate callback and optionstrings
380   my @options;
381   for my  $key (@itemstatus_keys, @callback_keys) {
382     next if ($form->{itemstatus} ne $key && !$form->{$key});
383     push @options, $optiontexts{$key};
384   }
385
386   # special case for lastcost
387   if ($form->{ledgerchecks}){
388     # ledgerchecks don't know about sellprice or lastcost. they just return a
389     # price. so rename sellprice to price, and drop lastcost.
390     $column_defs{sellprice}{text} = $locale->text('Price');
391     $form->{l_lastcost} = ""
392   }
393
394   if ($form->{description}) {
395     $description = $form->{description};
396     $description =~ s/\n/<br>/g;
397   }
398
399   if ($form->{l_linetotal}) {
400     $form->{l_qty} = "Y";
401     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
402     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
403     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
404   }
405   $form->{"l_type_and_classific"} = "Y";
406
407   if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
408
409     # remove bin, weight and rop from list
410     map { $form->{"l_$_"} = "" } qw(bin weight rop);
411
412     $form->{l_onhand} = "";
413
414     # qty is irrelevant unless bought or sold
415     if (   $form->{bought}
416         || $form->{sold}
417         || $form->{onorder}
418         || $form->{ordered}
419         || $form->{rfq}
420         || $form->{quoted}) {
421 #      $form->{l_onhand} = "Y";
422     } else {
423       $form->{l_linetotalsellprice} = "";
424       $form->{l_linetotallastcost}  = "";
425     }
426   }
427
428   # soldtotal doesn't make sense with more than one bsooqr option.
429   # so reset it to sold (the most common option), and issue a warning
430   # ...
431   # also it doesn't make sense without bsooqr. disable and issue a warning too
432   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
433   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
434   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
435     my $enabled       = first { $form->{$_} } @bsooqr;
436     $form->{$_}       = ''   for @bsooqr;
437     $form->{$enabled} = 'Y';
438
439     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
440   }
441   if ($form->{l_soldtotal} && !$bsooqr_mode) {
442     delete $form->{l_soldtotal};
443
444     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
445   }
446   if ($form->{l_name} && !$bsooqr_mode) {
447     delete $form->{l_name};
448
449     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
450   }
451   IC->all_parts(\%myconfig, \%$form);
452
453   my @columns = qw(
454     partnumber type_and_classific description notes partsgroup bin onhand rop soldtotal unit listprice
455     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
456     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
457     transdate name serialnumber deliverydate ean projectnumber projectdescription
458     insertdate shop
459   );
460
461   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
462   my @pricegroup_columns;
463   my %column_defs_pricegroups;
464   if ($form->{l_pricegroups}) {
465     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
466     %column_defs_pricegroups = map {
467       "pricegroup_" . $_->id => {
468         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
469         visible => 1,
470       },
471     }  @{ $pricegroups };
472   }
473   push @columns, @pricegroup_columns;
474
475   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
476   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
477   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
478
479   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
480
481   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
482   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
483   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
484
485   my @hidden_variables = (
486     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
487     qw(l_type_and_classific classification_id),
488     @itemstatus_keys,
489     @callback_keys,
490     map({ "cvar_$_->{name}" } @searchable_custom_variables),
491     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
492     map({ "l_$_" } @columns),
493   );
494
495   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
496
497   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
498   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
499
500   foreach my $col (@sort_full) {
501     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
502   }
503   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
504
505   # add order to callback
506   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
507
508   my $report = SL::ReportGenerator->new(\%myconfig, $form);
509
510   my %attachment_basenames = (
511     'part'     => $locale->text('part_list'),
512     'service'  => $locale->text('service_list'),
513     'assembly' => $locale->text('assembly_list'),
514     'article'  => $locale->text('article_list'),
515   );
516
517   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
518                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
519                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
520                        'output_format'         => 'HTML',
521                        'title'                 => $form->{title},
522                        'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
523   );
524   $report->set_options_from_form();
525   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
526
527   $report->set_columns(%column_defs);
528   $report->set_column_order(@columns);
529
530   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
531
532   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
533
534   CVar->add_custom_variables_to_report('module'         => 'IC',
535                                        'trans_id_field' => 'id',
536                                        'configs'        => $cvar_configs,
537                                        'column_defs'    => \%column_defs,
538                                        'data'           => $form->{parts});
539
540   CVar->add_custom_variables_to_report('module'         => 'IC',
541                                        'sub_module'     => sub { $_[0]->{ioi} },
542                                        'trans_id_field' => 'ioi_id',
543                                        'configs'        => $cvar_configs,
544                                        'column_defs'    => \%column_defs,
545                                        'data'           => $form->{parts});
546
547   my @subtotal_columns = qw(sellprice listprice lastcost);
548   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
549   my %totals    = map { $_ => 0 } @subtotal_columns;
550   my $idx       = 0;
551   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
552
553   my $defaults  = AM->get_defaults();
554
555   # postprocess parts
556   foreach my $ref (@{ $form->{parts} }) {
557
558     # fresh row, for inserting later
559     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
560
561     $ref->{exchangerate} ||= 1;
562     $ref->{price_factor} ||= 1;
563     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
564     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
565     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
566
567     # use this for assemblies
568     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
569
570     if ($ref->{assemblyitem}) {
571       $row->{partnumber}{align}   = 'right';
572       $row->{soldtotal}{data}     = 0;
573       $soldtotal                  = 0 if ($form->{sold});
574     }
575
576     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}), 'callback');
577     $row->{partnumber}->{link}  = $edit_link;
578     $row->{description}->{link} = $edit_link;
579
580     foreach (qw(sellprice listprice lastcost)) {
581       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
582       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
583     }
584     foreach ( @pricegroup_columns ) {
585       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
586     };
587
588
589     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
590
591     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
592
593     # 'yes' and 'no' for boolean value shop
594     if ($form->{l_shop}) {
595       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
596     }
597
598     if (!$ref->{assemblyitem}) {
599       foreach my $col (@subtotal_columns) {
600         $totals{$col}    += $soldtotal * $ref->{$col};
601         $subtotals{$col} += $soldtotal * $ref->{$col};
602       }
603
604       $subtotals{soldtotal} += $soldtotal;
605     }
606
607     # set module stuff
608     if ($ref->{module} eq 'oe') {
609       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
610       #
611       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
612       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
613
614       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');
615       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');
616
617       $row->{ordnumber}{link} = $edit_oe_ord_link;
618       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
619
620     } else {
621       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
622     }
623
624     # set properties of images
625     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
626       $row->{image}{data}     = '';
627       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
628     }
629     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
630
631     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
632     $row->{type_and_classific}{data} = $::request->presenter->type_abbreviation($ref->{part_type}).
633                                        $::request->presenter->classification_abbreviation($ref->{classification_id});
634
635     $report->add_data($row);
636
637     my $next_ref = $form->{parts}[$idx + 1];
638
639     # insert subtotal rows
640     if (($form->{l_subtotal} eq 'Y') &&
641         (!$next_ref ||
642          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
643       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
644
645       if ( !$form->{l_assembly} || !$form->{bom}) {
646         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
647       }
648
649       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
650       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
651
652       $report->add_data($row);
653
654       $same_item = $next_ref->{ $form->{sort} };
655     }
656
657     $idx++;
658   }
659
660   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
661     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
662
663     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
664
665     $report->add_separator();
666     $report->add_data($row);
667   }
668
669   $report->generate_with_headers();
670
671   $lxdebug->leave_sub();
672 }    #end generate_report
673
674 sub ajax_autocomplete {
675   $main::lxdebug->enter_sub();
676
677   my $form     = $main::form;
678   my %myconfig = %main::myconfig;
679
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}   ||= '';
684
685   my @results = IC->all_parts(\%myconfig, $form);
686
687   print $form->ajax_response_header(),
688         $form->parse_html_template('ic/ajax_autocomplete');
689
690   $main::lxdebug->leave_sub();
691 }
692
693 sub back_to_record {
694   _check_io_auth();
695
696
697   delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
698
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 });
703   $::form->redirect;
704 }
705
706 sub continue { call_sub($form->{"nextsub"}); }
707
708 sub dispatcher {
709   my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
710   $::form->error($::locale->text('No action defined.')) unless $action;
711
712   $::form->{dispatched_action} = $action;
713   call_sub($action);
714 }