450d124390c81240567b5e135ebf0bd012f1024a
[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     'assembly_lastcost'  => { 'text' => $locale->text('Assembly Last Cost'), },
181     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
182     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
183     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
184     'listprice'          => { 'text' => $locale->text('List Price'), },
185     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
186     'name'               => { 'text' => $locale->text('Name'), },
187     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
188     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
189     'partnumber'         => { 'text' => $locale->text('Part Number'), },
190     'partsgroup'         => { 'text' => $locale->text('Partsgroup'), },
191     'priceupdate'        => { 'text' => $locale->text('Price updated'), },
192     'quonumber'          => { 'text' => $locale->text('Quotation'), },
193     'rop'                => { 'text' => $locale->text('ROP'), },
194     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
195     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
196     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
197     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
198     'transdate'          => { 'text' => $locale->text('Transdate Record'), },
199     'unit'               => { 'text' => $locale->text('Unit'), },
200     'weight'             => { 'text' => $locale->text('Weight'), },
201     'shop'               => { 'text' => $locale->text('Shop article'), },
202     'type_and_classific' => { 'text' => $locale->text('Type'), },
203     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
204     'projectdescription' => { 'text' => $locale->text('Project Description'), },
205     'warehouse'          => { 'text' => $locale->text('Default Warehouse'), },
206     'bin'                => { 'text' => $locale->text('Default Bin'), },
207     'make'               => { 'text' => $locale->text('Make'), },
208     'model'              => { 'text' => $locale->text('Model'), },
209   );
210
211   $revers     = $form->{revers};
212   $lastsort   = $form->{lastsort};
213
214   # sorting and direction of sorting
215   # ToDO: change this to the simpler field+direction method
216   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
217     $form->{revers}   = 0;
218     $form->{lastsort} = "partnumber";
219     $form->{sort}     = "partnumber";
220   } else {
221     if ($form->{lastsort} eq $form->{sort}) {
222       $form->{revers} = 1 - $form->{revers};
223     } else {
224       $form->{revers} = 0;
225       $form->{lastsort} = $form->{sort};
226     }    #fi
227   }    #fi
228
229   # special case if we have a serialnumber limit search
230   # serialnumbers are only given in invoices and orders,
231   # so they can only pop up in bought, sold, rfq, and quoted stuff
232   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
233                                  && !$form->{rfq}    && !$form->{quoted}
234                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
235
236   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
237   # if any of these are ticked the behavior changes slightly for lastcost
238   # since all those are aggregation checks for the legder tables this is an internal switch
239   # refered to as ledgerchecks
240   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
241                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
242
243   # if something should be activated if something else is active, enter it here
244   my %dependencies = (
245     onhand       => [ qw(l_onhand) ],
246     short        => [ qw(l_onhand) ],
247     onorder      => [ qw(l_ordnumber) ],
248     ordered      => [ qw(l_ordnumber) ],
249     rfq          => [ qw(l_quonumber) ],
250     quoted       => [ qw(l_quonumber) ],
251     bought       => [ qw(l_invnumber) ],
252     sold         => [ qw(l_invnumber) ],
253     ledgerchecks => [ qw(l_name) ],
254     serialnumber => [ qw(l_serialnumber) ],
255     no_sn_joins  => [ qw(bought sold) ],
256   );
257
258   # get name of partsgroup if id is given
259   my $pg_name;
260   if ($form->{partsgroup_id}) {
261     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
262     $pg_name = $pg->{'partsgroup'};
263   }
264
265   # these strings get displayed at the top of the results to indicate the user which switches were used
266   my %optiontexts = (
267     active        => $locale->text('Active'),
268     obsolete      => $locale->text('Obsolete'),
269     orphaned      => $locale->text('Orphaned'),
270     onhand        => $locale->text('On Hand'),
271     short         => $locale->text('Short'),
272     onorder       => $locale->text('On Order'),
273     ordered       => $locale->text('Ordered'),
274     rfq           => $locale->text('RFQ'),
275     quoted        => $locale->text('Quoted'),
276     bought        => $locale->text('Bought'),
277     sold          => $locale->text('Sold'),
278     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
279     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
280     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
281     partsgroup    => $locale->text('Partsgroup')       . ": '$form->{partsgroup}'",
282     partsgroup_id => $locale->text('Partsgroup')       . ": '$pg_name'",
283     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
284     description   => $locale->text('Part Description') . ": '$form->{description}'",
285     make          => $locale->text('Make')             . ": '$form->{make}'",
286     model         => $locale->text('Model')            . ": '$form->{model}'",
287     customername  => $locale->text('Customer')         . ": '$form->{customername}'",
288     customernumber=> $locale->text('Customer Part Number').": '$form->{customernumber}'",
289     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
290     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
291     l_soldtotal   => $locale->text('Qty in Selected Records'),
292     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
293     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
294     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
295     l_service     => $locale->text('Services'),
296     l_assembly    => $locale->text('Assemblies'),
297     l_part        => $locale->text('Parts'),
298   );
299
300   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
301   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
302                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all
303                            l_service l_assembly l_part);
304
305   # calculate dependencies
306   for (@itemstatus_keys, @callback_keys) {
307     next if ($form->{itemstatus} ne $_ && !$form->{$_});
308     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
309   }
310
311   # generate callback and optionstrings
312   my @options;
313   for my  $key (@itemstatus_keys, @callback_keys) {
314     next if ($form->{itemstatus} ne $key && !$form->{$key});
315     push @options, $optiontexts{$key};
316   }
317
318   # special case for lastcost
319   if ($form->{ledgerchecks}){
320     # ledgerchecks don't know about sellprice or lastcost. they just return a
321     # price. so rename sellprice to price, and drop lastcost.
322     $column_defs{sellprice}{text} = $locale->text('Price');
323     $form->{l_lastcost} = ""
324   }
325   $form->{l_assembly_lastcost} = "Y" if $form->{l_assembly} && $form->{l_lastcost};
326
327   if ($form->{description}) {
328     $description = $form->{description};
329     $description =~ s/\n/<br>/g;
330   }
331
332   if ($form->{l_linetotal}) {
333     $form->{l_qty} = "Y";
334     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
335     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
336     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
337   }
338   $form->{"l_type_and_classific"} = "Y";
339
340   if ($form->{l_service} && !$form->{l_assembly} && !$form->{l_part}) {
341
342     # remove warehouse, bin, weight and rop from list
343     map { $form->{"l_$_"} = "" } qw(bin weight rop warehouse);
344
345     $form->{l_onhand} = "";
346
347     # qty is irrelevant unless bought or sold
348     if (   $form->{bought}
349         || $form->{sold}
350         || $form->{onorder}
351         || $form->{ordered}
352         || $form->{rfq}
353         || $form->{quoted}) {
354 #      $form->{l_onhand} = "Y";
355     } else {
356       $form->{l_linetotalsellprice} = "";
357       $form->{l_linetotallastcost}  = "";
358     }
359   }
360
361   # soldtotal doesn't make sense with more than one bsooqr option.
362   # so reset it to sold (the most common option), and issue a warning
363   # ...
364   # also it doesn't make sense without bsooqr. disable and issue a warning too
365   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
366   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
367   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
368     my $enabled       = first { $form->{$_} } @bsooqr;
369     $form->{$_}       = ''   for @bsooqr;
370     $form->{$enabled} = 'Y';
371
372     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
373   }
374   if ($form->{l_soldtotal} && !$bsooqr_mode) {
375     delete $form->{l_soldtotal};
376
377     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
378   }
379   if ($form->{l_soldtotal} && ($form->{l_warehouse} || $form->{l_bin})) {
380     delete $form->{"l_$_"} for  qw(bin warehouse);
381     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.'));
382   }
383   if ($form->{l_name} && !$bsooqr_mode) {
384     delete $form->{l_name};
385
386     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
387   }
388   IC->all_parts(\%myconfig, \%$form);
389
390   my @columns = qw(
391     partnumber type_and_classific description notes partsgroup warehouse bin
392     make model onhand rop soldtotal unit listprice
393     linetotallistprice sellprice linetotalsellprice lastcost assembly_lastcost linetotallastcost
394     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
395     transdate name serialnumber deliverydate ean projectnumber projectdescription
396     insertdate shop
397   );
398
399   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
400   my @pricegroup_columns;
401   my %column_defs_pricegroups;
402   if ($form->{l_pricegroups}) {
403     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
404     %column_defs_pricegroups = map {
405       "pricegroup_" . $_->id => {
406         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
407         visible => 1,
408       },
409     }  @{ $pricegroups };
410   }
411   push @columns, @pricegroup_columns;
412
413   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
414   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
415   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
416
417   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
418
419   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
420   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
421   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost assembly_lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
422
423   my @hidden_variables = (
424     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
425     qw(l_type_and_classific classification_id l_part l_service l_assembly l_assortment),
426     @itemstatus_keys,
427     @callback_keys,
428     map({ "cvar_$_->{name}" } @searchable_custom_variables),
429     map({'cvar_'. $_->{name} .'_from'} grep({$_->{type} eq 'date'} @searchable_custom_variables)),
430     map({'cvar_'. $_->{name} .'_to'}   grep({$_->{type} eq 'date'} @searchable_custom_variables)),
431     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
432     map({ "l_$_" } @columns),
433   );
434
435   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
436
437   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
438   my @sort_no_revers   = qw(partsgroup invnumber ordnumber quonumber name image drawing serialnumber);
439
440   foreach my $col (@sort_full) {
441     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
442   }
443   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
444
445   # add order to callback
446   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
447
448   my $report = SL::ReportGenerator->new(\%myconfig, $form);
449
450   my %attachment_basenames = (
451     'part'     => $locale->text('part_list'),
452     'service'  => $locale->text('service_list'),
453     'assembly' => $locale->text('assembly_list'),
454     'article'  => $locale->text('article_list'),
455   );
456
457   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
458                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom' ,
459                                                   { PART_CLASSIFICATIONS => SL::DB::Manager::PartClassification->get_all_sorted }),
460                        'output_format'         => 'HTML',
461                        'title'                 => $form->{title},
462                        'attachment_basename'   => 'article_list' . strftime('_%Y%m%d', localtime time),
463   );
464   $report->set_options_from_form();
465   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
466
467   $report->set_columns(%column_defs);
468   $report->set_column_order(@columns);
469
470   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
471
472   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
473
474   CVar->add_custom_variables_to_report('module'         => 'IC',
475                                        'trans_id_field' => 'id',
476                                        'configs'        => $cvar_configs,
477                                        'column_defs'    => \%column_defs,
478                                        'data'           => $form->{parts});
479
480   CVar->add_custom_variables_to_report('module'         => 'IC',
481                                        'sub_module'     => sub { $_[0]->{ioi} },
482                                        'trans_id_field' => 'ioi_id',
483                                        'configs'        => $cvar_configs,
484                                        'column_defs'    => \%column_defs,
485                                        'data'           => $form->{parts});
486
487   my @subtotal_columns = qw(sellprice listprice lastcost);
488   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
489   my %totals    = map { $_ => 0 } @subtotal_columns;
490   my $idx       = 0;
491   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
492
493   my $defaults  = AM->get_defaults();
494
495   # postprocess parts
496   foreach my $ref (@{ $form->{parts} }) {
497
498     # fresh row, for inserting later
499     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
500
501     $ref->{exchangerate} ||= 1;
502     $ref->{price_factor} ||= 1;
503     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
504     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
505     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
506     $ref->{assembly_lastcost} *= $ref->{exchangerate} / $ref->{price_factor};
507
508     # use this for assemblies
509     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
510
511     if ($ref->{assemblyitem}) {
512       $row->{partnumber}{align}   = 'right';
513       $row->{soldtotal}{data}     = 0;
514       $soldtotal                  = 0 if ($form->{sold});
515     }
516
517     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}));
518     $row->{partnumber}->{link}  = $edit_link;
519     $row->{description}->{link} = $edit_link;
520
521     foreach (qw(sellprice listprice lastcost assembly_lastcost)) {
522       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
523       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
524     }
525     foreach ( @pricegroup_columns ) {
526       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
527     };
528
529
530     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
531
532     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
533
534     # 'yes' and 'no' for boolean value shop
535     if ($form->{l_shop}) {
536       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
537     }
538
539     if (!$ref->{assemblyitem}) {
540       foreach my $col (@subtotal_columns) {
541         $totals{$col}    += $soldtotal * $ref->{$col};
542         $subtotals{$col} += $soldtotal * $ref->{$col};
543       }
544
545       $subtotals{soldtotal} += $soldtotal;
546     }
547
548     # set module stuff
549     if ($ref->{module} eq 'oe') {
550       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
551       #
552       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
553       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
554
555       my $edit_oe_ord_link = ($::instance_conf->get_feature_experimental_order)
556                            ? build_std_url("script=controller.pl", 'action=Order/edit',
557                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback')
558                            : build_std_url("script=oe.pl",         'action=edit',
559                                            'type=' . E($ref->{cv} eq 'vendor' ? 'purchase_order' : 'sales_order'),        'id=' . E($ref->{trans_id}), 'callback');
560
561       my $edit_oe_quo_link = ($::instance_conf->get_feature_experimental_order)
562                            ? build_std_url("script=controller.pl", 'action=Order/edit',
563                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback')
564                            : build_std_url("script=oe.pl",         'action=edit',
565                                            'type=' . E($ref->{cv} eq 'vendor' ? 'request_quotation' : 'sales_quotation'), 'id=' . E($ref->{trans_id}), 'callback');
566
567       $row->{ordnumber}{link} = $edit_oe_ord_link;
568       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
569
570     } else {
571       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
572     }
573
574     # set properties of images
575     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
576       $row->{image}{data}     = '';
577       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
578     }
579     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
580
581     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
582     $row->{type_and_classific}{data} = SL::Presenter::Part::type_abbreviation($ref->{part_type}).
583                                        SL::Presenter::Part::classification_abbreviation($ref->{classification_id});
584
585     # last price update
586     $row->{priceupdate}{data} = SL::DB::Part->new(id => $ref->{id})->load->last_price_update->valid_from->to_kivitendo;
587
588     $report->add_data($row);
589
590     my $next_ref = $form->{parts}[$idx + 1];
591
592     # insert subtotal rows
593     if (($form->{l_subtotal} eq 'Y') &&
594         (!$next_ref ||
595          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
596       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
597
598       if ( !$form->{l_assembly} || !$form->{bom}) {
599         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
600       }
601
602       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
603       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
604
605       $report->add_data($row);
606
607       $same_item = $next_ref->{ $form->{sort} };
608     }
609
610     $idx++;
611   }
612
613   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
614     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
615
616     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
617
618     $report->add_separator();
619     $report->add_data($row);
620   }
621
622   setup_ic_generate_report_action_bar();
623   $report->generate_with_headers();
624
625   $lxdebug->leave_sub();
626 }    #end generate_report
627
628 sub setup_ic_search_action_bar {
629   my %params = @_;
630
631   for my $bar ($::request->layout->get('actionbar')) {
632     $bar->add(
633       action => [
634         t8('Search'),
635         submit    => [ '#form', { action => 'generate_report' } ],
636         accesskey => 'enter',
637       ],
638
639       action => [
640         t8('TOP100'),
641         submit => [ '#form', { action => 'top100' } ],
642       ],
643     );
644   }
645 }
646
647 sub setup_ic_generate_report_action_bar {
648   my %params = @_;
649
650   for my $bar ($::request->layout->get('actionbar')) {
651     $bar->add(
652       combobox => [
653         action => [
654           t8('Add'),
655         ],
656         action => [
657           t8('Add Part'),
658           submit    => [ '#new_form', { action => 'Part/add_part' } ],
659           accesskey => 'enter',
660         ],
661         action => [
662           t8('Add Service'),
663           submit    => [ '#new_form', { action => 'Part/add_service' } ],
664         ],
665         action => [
666           t8('Add Assembly'),
667           submit    => [ '#new_form', { action => 'Part/add_assembly' } ],
668         ],
669         action => [
670           t8('Add Assortment'),
671           submit    => [ '#new_form', { action => 'Part/add_assortment' } ],
672         ],
673       ], # end of combobox "Add part"
674     );
675   }
676 }