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