Revert "Artikel-Klassifizierung"
[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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 #======================================================================
29 #
30 # Inventory Control module
31 #
32 #======================================================================
33
34 use POSIX qw(strftime);
35 use List::Util qw(first max);
36 use List::MoreUtils qw(any);
37
38 use SL::AM;
39 use SL::CVar;
40 use SL::IC;
41 use SL::Helper::Flash qw(flash);
42 use SL::HTML::Util;
43 use SL::ReportGenerator;
44
45 #use SL::PE;
46
47 use strict;
48 #use warnings;
49
50 # global imports
51 our ($form, $locale, %myconfig, $lxdebug, $auth);
52
53 require "bin/mozilla/io.pl";
54 require "bin/mozilla/common.pl";
55 require "bin/mozilla/reportgenerator.pl";
56
57 1;
58
59 # Parserhappy(R):
60 # type=submit $locale->text('Add Part')
61 # type=submit $locale->text('Add Service')
62 # type=submit $locale->text('Add Assembly')
63 # type=submit $locale->text('Edit Part')
64 # type=submit $locale->text('Edit Service')
65 # type=submit $locale->text('Edit Assembly')
66 # $locale->text('Parts')
67 # $locale->text('Services')
68 # $locale->text('Inventory quantity must be zero before you can set this part obsolete!')
69 # $locale->text('Inventory quantity must be zero before you can set this assembly obsolete!')
70 # $locale->text('Part Number missing!')
71 # $locale->text('Service Number missing!')
72 # $locale->text('Assembly Number missing!')
73 # $locale->text('ea');
74
75 # end of main
76
77 sub add {
78   $lxdebug->enter_sub();
79
80   $auth->assert('part_service_assembly_edit');
81
82   my $title                = 'Add ' . ucfirst $form->{part_type};
83   $form->{title}           = $locale->text($title);
84   $form->{callback}        = "$form->{script}?action=add&part_type=$form->{part_type}" unless $form->{callback};
85   $form->{unit_changeable} = 1;
86
87   IC->get_pricegroups(\%myconfig, \%$form);
88   &link_part;
89   &display_form;
90
91   $lxdebug->leave_sub();
92 }
93
94 sub search {
95   $lxdebug->enter_sub();
96
97   $auth->assert('part_service_assembly_details');
98
99   $form->{revers}       = 0;  # switch for backward sorting
100   $form->{lastsort}     = ""; # memory for which table was sort at last time
101   $form->{ndxs_counter} = 0;  # counter for added entries to top100
102
103   my %is_xyz     = map { +"is_$_" => ($form->{searchitems} eq $_) } qw(part service assembly);
104
105   $form->{title} = (ucfirst $form->{searchitems}) . "s";
106   $form->{title} = $locale->text($form->{title});
107   $form->{title} = $locale->text('Assemblies') if ($is_xyz{is_assembly});
108
109   $form->{CUSTOM_VARIABLES}                  = CVar->get_configs('module' => 'IC');
110   ($form->{CUSTOM_VARIABLES_FILTER_CODE},
111    $form->{CUSTOM_VARIABLES_INCLUSION_CODE}) = CVar->render_search_options('variables'      => $form->{CUSTOM_VARIABLES},
112                                                                            'include_prefix' => 'l_',
113                                                                            'include_value'  => 'Y');
114
115   $form->header;
116
117   $form->get_lists('partsgroup'    => 'ALL_PARTSGROUPS');
118   print $form->parse_html_template('ic/search', { %is_xyz, });
119
120   $lxdebug->leave_sub();
121 }    #end search()
122
123 sub search_update_prices {
124   $lxdebug->enter_sub();
125
126   $auth->assert('part_service_assembly_edit');
127
128   my $pricegroups = IC->get_pricegroups(\%myconfig, \%$form);
129
130   $form->{title} = $locale->text('Update Prices');
131
132   $form->header;
133
134   print $form->parse_html_template('ic/search_update_prices', { PRICE_ROWS => $pricegroups });
135
136   $lxdebug->leave_sub();
137 }    #end search()
138
139 sub confirm_price_update {
140   $lxdebug->enter_sub();
141
142   $auth->assert('part_service_assembly_edit');
143
144   my @errors      = ();
145   my $value_found = undef;
146
147   foreach my $idx (qw(sellprice listprice), (1..$form->{price_rows})) {
148     my $name      = $idx =~ m/\d/ ? $form->{"pricegroup_${idx}"}      : $idx eq 'sellprice' ? $locale->text('Sell Price') : $locale->text('List Price');
149     my $type      = $idx =~ m/\d/ ? $form->{"pricegroup_type_${idx}"} : $form->{"${idx}_type"};
150     my $value_idx = $idx =~ m/\d/ ? "price_${idx}" : $idx;
151     my $value     = $form->parse_amount(\%myconfig, $form->{$value_idx});
152
153     if ((0 > $value) && ($type eq 'percent')) {
154       push @errors, $locale->text('You cannot adjust the price for pricegroup "#1" by a negative percentage.', $name);
155
156     } elsif (!$value && ($form->{$value_idx} ne '')) {
157       push @errors, $locale->text('No valid number entered for pricegroup "#1".', $name);
158
159     } elsif (0 < $value) {
160       $value_found = 1;
161     }
162   }
163
164   push @errors, $locale->text('No prices will be updated because no prices have been entered.') if (!$value_found);
165
166   my $num_matches = IC->get_num_matches_for_priceupdate();
167
168   $form->header();
169
170   if (@errors) {
171     $form->show_generic_error(join('<br>', @errors), 'back_button' => 1);
172   }
173
174   $form->{nextsub} = "update_prices";
175
176   map { delete $form->{$_} } qw(action header);
177
178   print $form->parse_html_template('ic/confirm_price_update', { HIDDENS     => [ map { name => $_, value => $form->{$_} }, keys %$form ],
179                                                                 num_matches => $num_matches });
180
181   $lxdebug->leave_sub();
182 }
183
184 sub update_prices {
185   $lxdebug->enter_sub();
186
187   $auth->assert('part_service_assembly_edit');
188
189   my $num_updated = IC->update_prices(\%myconfig, \%$form);
190
191   if (-1 != $num_updated) {
192     $form->redirect($locale->text('#1 prices were updated.', $num_updated));
193   } else {
194     $form->error($locale->text('Could not update prices!'));
195   }
196
197   $lxdebug->leave_sub();
198 }
199
200 sub top100 {
201   $::lxdebug->enter_sub();
202
203   $::auth->assert('part_service_assembly_edit');
204
205   $::form->{l_soldtotal} = "Y";
206   $::form->{sort}        = "soldtotal";
207   $::form->{lastsort}    = "soldtotal";
208
209   $::form->{l_qty}       = undef;
210   $::form->{l_linetotal} = undef;
211   $::form->{l_number}    = "Y";
212   $::form->{number}      = "position";
213
214   unless (   $::form->{bought}
215           || $::form->{sold}
216           || $::form->{rfq}
217           || $::form->{quoted}) {
218     $::form->{bought} = $::form->{sold} = 1;
219   }
220
221   generate_report();
222
223   $lxdebug->leave_sub();
224 }
225
226 #
227 # Report for Wares.
228 # Warning, deep magic ahead.
229 # This function parses the requested details, sanity checks them, and converts them into a format thats usable for IC->all_parts
230 #
231 # flags coming from the form:
232 # hardcoded:
233 #  searchitems=part revers=0 lastsort=''
234 #
235 # filter:
236 # partnumber ean description partsgroup serialnumber make model drawing microfiche
237 # transdatefrom transdateto
238 #
239 # radio:
240 #  itemstatus = active | onhand | short | obsolete | orphaned
241 #  action     = continue | top100
242 #
243 # checkboxes:
244 #  bought sold onorder ordered rfq quoted
245 #  l_partnumber l_description l_serialnumber l_unit l_listprice l_sellprice l_lastcost
246 #  l_linetotal l_priceupdate l_bin l_rop l_weight l_image l_drawing l_microfiche
247 #  l_partsgroup l_subtotal l_soldtotal l_deliverydate l_pricegroups
248 #
249 # hiddens:
250 #  nextsub revers lastsort sort ndxs_counter
251 #
252 sub generate_report {
253   $lxdebug->enter_sub();
254
255   $auth->assert('part_service_assembly_details');
256
257   my ($revers, $lastsort, $description);
258
259   my $cvar_configs = CVar->get_configs('module' => 'IC');
260
261   my %titles = (
262     ''         => $locale->text('Articles'),
263     part       => $locale->text('Parts'),
264     service    => $locale->text('Services'),
265     assembly   => $locale->text('Assemblies'),
266     assortment => $locale->text('Assortments'),
267   );
268
269   $form->{title} = $titles{$form->{searchitems}};
270
271   my %column_defs = (
272     'bin'                => { 'text' => $locale->text('Bin'), },
273     'deliverydate'       => { 'text' => $locale->text('deliverydate'), },
274     'description'        => { 'text' => $locale->text('Part Description'), },
275     'notes'              => { 'text' => $locale->text('Notes'), },
276     'drawing'            => { 'text' => $locale->text('Drawing'), },
277     'ean'                => { 'text' => $locale->text('EAN'), },
278     'image'              => { 'text' => $locale->text('Image'), },
279     'insertdate'         => { 'text' => $locale->text('Insert Date'), },
280     'invnumber'          => { 'text' => $locale->text('Invoice Number'), },
281     'lastcost'           => { 'text' => $locale->text('Last Cost'), },
282     'linetotallastcost'  => { 'text' => $locale->text('Extended'), },
283     'linetotallistprice' => { 'text' => $locale->text('Extended'), },
284     'linetotalsellprice' => { 'text' => $locale->text('Extended'), },
285     'listprice'          => { 'text' => $locale->text('List Price'), },
286     'microfiche'         => { 'text' => $locale->text('Microfiche'), },
287     'name'               => { 'text' => $locale->text('Name'), },
288     'onhand'             => { 'text' => $locale->text('Stocked Qty'), },
289     'ordnumber'          => { 'text' => $locale->text('Order Number'), },
290     'partnumber'         => { 'text' => $locale->text('Part Number'), },
291     'partsgroup'         => { 'text' => $locale->text('Group'), },
292     'priceupdate'        => { 'text' => $locale->text('Updated'), },
293     'quonumber'          => { 'text' => $locale->text('Quotation'), },
294     'rop'                => { 'text' => $locale->text('ROP'), },
295     'sellprice'          => { 'text' => $locale->text('Sell Price'), },
296     'serialnumber'       => { 'text' => $locale->text('Serial Number'), },
297     'soldtotal'          => { 'text' => $locale->text('Qty in Selected Records'), },
298     'name'               => { 'text' => $locale->text('Name in Selected Records'), },
299     'transdate'          => { 'text' => $locale->text('Transdate'), },
300     'unit'               => { 'text' => $locale->text('Unit'), },
301     'weight'             => { 'text' => $locale->text('Weight'), },
302     'shop'               => { 'text' => $locale->text('Shop article'), },
303     'projectnumber'      => { 'text' => $locale->text('Project Number'), },
304     'projectdescription' => { 'text' => $locale->text('Project Description'), },
305   );
306
307   $revers     = $form->{revers};
308   $lastsort   = $form->{lastsort};
309
310   # sorting and direction of sorting
311   # ToDO: change this to the simpler field+direction method
312   if (($form->{lastsort} eq "") && ($form->{sort} eq undef)) {
313     $form->{revers}   = 0;
314     $form->{lastsort} = "partnumber";
315     $form->{sort}     = "partnumber";
316   } else {
317     if ($form->{lastsort} eq $form->{sort}) {
318       $form->{revers} = 1 - $form->{revers};
319     } else {
320       $form->{revers} = 0;
321       $form->{lastsort} = $form->{sort};
322     }    #fi
323   }    #fi
324
325   # special case if we have a serialnumber limit search
326   # serialnumbers are only given in invoices and orders,
327   # so they can only pop up in bought, sold, rfq, and quoted stuff
328   $form->{no_sn_joins} = 'Y' if (   !$form->{bought} && !$form->{sold}
329                                  && !$form->{rfq}    && !$form->{quoted}
330                                  && ($form->{l_serialnumber} || $form->{serialnumber}));
331
332   # special case for any checkbox of bought | sold | onorder | ordered | rfq | quoted.
333   # if any of these are ticked the behavior changes slightly for lastcost
334   # since all those are aggregation checks for the legder tables this is an internal switch
335   # refered to as ledgerchecks
336   $form->{ledgerchecks} = 'Y' if (   $form->{bought} || $form->{sold} || $form->{onorder}
337                                   || $form->{ordered} || $form->{rfq} || $form->{quoted});
338
339   # if something should be activated if something else is active, enter it here
340   my %dependencies = (
341     onhand       => [ qw(l_onhand) ],
342     short        => [ qw(l_onhand) ],
343     onorder      => [ qw(l_ordnumber) ],
344     ordered      => [ qw(l_ordnumber) ],
345     rfq          => [ qw(l_quonumber) ],
346     quoted       => [ qw(l_quonumber) ],
347     bought       => [ qw(l_invnumber) ],
348     sold         => [ qw(l_invnumber) ],
349     ledgerchecks => [ qw(l_name) ],
350     serialnumber => [ qw(l_serialnumber) ],
351     no_sn_joins  => [ qw(bought sold) ],
352   );
353
354   # get name of partsgroup if id is given
355   my $pg_name;
356   if ($form->{partsgroup_id}) {
357     my $pg = SL::DB::PartsGroup->new(id => $form->{partsgroup_id})->load;
358     $pg_name = $pg->{'partsgroup'};
359   }
360
361   # these strings get displayed at the top of the results to indicate the user which switches were used
362   my %optiontexts = (
363     active        => $locale->text('Active'),
364     obsolete      => $locale->text('Obsolete'),
365     orphaned      => $locale->text('Orphaned'),
366     onhand        => $locale->text('On Hand'),
367     short         => $locale->text('Short'),
368     onorder       => $locale->text('On Order'),
369     ordered       => $locale->text('Ordered'),
370     rfq           => $locale->text('RFQ'),
371     quoted        => $locale->text('Quoted'),
372     bought        => $locale->text('Bought'),
373     sold          => $locale->text('Sold'),
374     transdatefrom => $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{transdatefrom}, 1),
375     transdateto   => $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{transdateto}, 1),
376     partnumber    => $locale->text('Part Number')      . ": '$form->{partnumber}'",
377     partsgroup    => $locale->text('Group')            . ": '$form->{partsgroup}'",
378     partsgroup_id => $locale->text('Group')            . ": '$pg_name'",
379     serialnumber  => $locale->text('Serial Number')    . ": '$form->{serialnumber}'",
380     description   => $locale->text('Part Description') . ": '$form->{description}'",
381     make          => $locale->text('Make')             . ": '$form->{make}'",
382     model         => $locale->text('Model')            . ": '$form->{model}'",
383     drawing       => $locale->text('Drawing')          . ": '$form->{drawing}'",
384     microfiche    => $locale->text('Microfiche')       . ": '$form->{microfiche}'",
385     l_soldtotal   => $locale->text('Qty in Selected Records'),
386     ean           => $locale->text('EAN')              . ": '$form->{ean}'",
387     insertdatefrom => $locale->text('Insert Date') . ": " . $locale->text('From')       . " " . $locale->date(\%myconfig, $form->{insertdatefrom}, 1),
388     insertdateto   => $locale->text('Insert Date') . ": " . $locale->text('To (time)')  . " " . $locale->date(\%myconfig, $form->{insertdateto}, 1),
389   );
390
391   my @itemstatus_keys = qw(active obsolete orphaned onhand short);
392   my @callback_keys   = qw(onorder ordered rfq quoted bought sold partnumber partsgroup partsgroup_id serialnumber description make model
393                            drawing microfiche l_soldtotal l_deliverydate transdatefrom transdateto insertdatefrom insertdateto ean shop all);
394
395   # calculate dependencies
396   for (@itemstatus_keys, @callback_keys) {
397     next if ($form->{itemstatus} ne $_ && !$form->{$_});
398     map { $form->{$_} = 'Y' } @{ $dependencies{$_} } if $dependencies{$_};
399   }
400
401   # generate callback and optionstrings
402   my @options;
403   for my  $key (@itemstatus_keys, @callback_keys) {
404     next if ($form->{itemstatus} ne $key && !$form->{$key});
405     push @options, $optiontexts{$key};
406   }
407
408   # special case for lastcost
409   if ($form->{ledgerchecks}){
410     # ledgerchecks don't know about sellprice or lastcost. they just return a
411     # price. so rename sellprice to price, and drop lastcost.
412     $column_defs{sellprice}{text} = $locale->text('Price');
413     $form->{l_lastcost} = ""
414   }
415
416   if ($form->{description}) {
417     $description = $form->{description};
418     $description =~ s/\n/<br>/g;
419   }
420
421   if ($form->{l_linetotal}) {
422     $form->{l_qty} = "Y";
423     $form->{l_linetotalsellprice} = "Y" if $form->{l_sellprice};
424     $form->{l_linetotallastcost}  = $form->{searchitems} eq 'assembly' && !$form->{bom} ? "" : 'Y' if  $form->{l_lastcost};
425     $form->{l_linetotallistprice} = "Y" if $form->{l_listprice};
426   }
427
428   if ($form->{searchitems} eq 'service') {
429
430     # remove bin, weight and rop from list
431     map { $form->{"l_$_"} = "" } qw(bin weight rop);
432
433     $form->{l_onhand} = "";
434
435     # qty is irrelevant unless bought or sold
436     if (   $form->{bought}
437         || $form->{sold}
438         || $form->{onorder}
439         || $form->{ordered}
440         || $form->{rfq}
441         || $form->{quoted}) {
442 #      $form->{l_onhand} = "Y";
443     } else {
444       $form->{l_linetotalsellprice} = "";
445       $form->{l_linetotallastcost}  = "";
446     }
447   }
448
449   # soldtotal doesn't make sense with more than one bsooqr option.
450   # so reset it to sold (the most common option), and issue a warning
451   # ...
452   # also it doesn't make sense without bsooqr. disable and issue a warning too
453   my @bsooqr = qw(sold bought onorder ordered rfq quoted);
454   my $bsooqr_mode = grep { $form->{$_} } @bsooqr;
455   if ($form->{l_subtotal} && 1 < $bsooqr_mode) {
456     my $enabled       = first { $form->{$_} } @bsooqr;
457     $form->{$_}       = ''   for @bsooqr;
458     $form->{$enabled} = 'Y';
459
460     push @options, $::locale->text('Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1', $optiontexts{$enabled});
461   }
462   if ($form->{l_soldtotal} && !$bsooqr_mode) {
463     delete $form->{l_soldtotal};
464
465     flash('warning', $::locale->text('Soldtotal does not make sense without any bsooqr options'));
466   }
467   if ($form->{l_name} && !$bsooqr_mode) {
468     delete $form->{l_name};
469
470     flash('warning', $::locale->text('Name does not make sense without any bsooqr options'));
471   }
472   IC->all_parts(\%myconfig, \%$form);
473
474   my @columns = qw(
475     partnumber description notes partsgroup bin onhand rop soldtotal unit listprice
476     linetotallistprice sellprice linetotalsellprice lastcost linetotallastcost
477     priceupdate weight image drawing microfiche invnumber ordnumber quonumber
478     transdate name serialnumber deliverydate ean projectnumber projectdescription
479     insertdate shop
480   );
481
482   my $pricegroups = SL::DB::Manager::Pricegroup->get_all_sorted;
483   my @pricegroup_columns;
484   my %column_defs_pricegroups;
485   if ($form->{l_pricegroups}) {
486     @pricegroup_columns      = map { "pricegroup_" . $_->id } @{ $pricegroups };
487     %column_defs_pricegroups = map {
488       "pricegroup_" . $_->id => {
489         text    => $::locale->text('Pricegroup') . ' ' . $_->pricegroup,
490         visible => 1,
491       },
492     }  @{ $pricegroups };
493   }
494   push @columns, @pricegroup_columns;
495
496   my @includeable_custom_variables = grep { $_->{includeable} } @{ $cvar_configs };
497   my @searchable_custom_variables  = grep { $_->{searchable} }  @{ $cvar_configs };
498   my %column_defs_cvars            = map { +"cvar_$_->{name}" => { 'text' => $_->{description} } } @includeable_custom_variables;
499
500   push @columns, map { "cvar_$_->{name}" } @includeable_custom_variables;
501
502   %column_defs = (%column_defs, %column_defs_cvars, %column_defs_pricegroups);
503   map { $column_defs{$_}->{visible} ||= $form->{"l_$_"} ? 1 : 0 } @columns;
504   map { $column_defs{$_}->{align}   = 'right' } qw(onhand sellprice listprice lastcost linetotalsellprice linetotallastcost linetotallistprice rop weight soldtotal shop), @pricegroup_columns;
505
506   my @hidden_variables = (
507     qw(l_subtotal l_linetotal searchitems itemstatus bom l_pricegroups insertdatefrom insertdateto),
508     @itemstatus_keys,
509     @callback_keys,
510     map({ "cvar_$_->{name}" } @searchable_custom_variables),
511     map({'cvar_'. $_->{name} .'_qtyop'} grep({$_->{type} eq 'number'} @searchable_custom_variables)),
512     map({ "l_$_" } @columns),
513   );
514
515   my $callback         = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
516
517   my @sort_full        = qw(partnumber description onhand soldtotal deliverydate insertdate shop);
518   my @sort_no_revers   = qw(partsgroup bin priceupdate invnumber ordnumber quonumber name image drawing serialnumber);
519
520   foreach my $col (@sort_full) {
521     $column_defs{$col}->{link} = join '&', $callback, "sort=$col", map { "$_=" . E($form->{$_}) } qw(revers lastsort);
522   }
523   map { $column_defs{$_}->{link} = "${callback}&sort=$_" } @sort_no_revers;
524
525   # add order to callback
526   $form->{callback} = join '&', ($callback, map { "${_}=" . E($form->{$_}) } qw(sort revers));
527
528   my $report = SL::ReportGenerator->new(\%myconfig, $form);
529
530   my %attachment_basenames = (
531     'part'     => $locale->text('part_list'),
532     'service'  => $locale->text('service_list'),
533     'assembly' => $locale->text('assembly_list'),
534   );
535
536   $report->set_options('raw_top_info_text'     => $form->parse_html_template('ic/generate_report_top', { options => \@options }),
537                        'raw_bottom_info_text'  => $form->parse_html_template('ic/generate_report_bottom'),
538                        'output_format'         => 'HTML',
539                        'title'                 => $form->{title},
540                        'attachment_basename'   => $attachment_basenames{$form->{searchitems}} . strftime('_%Y%m%d', localtime time),
541   );
542   $report->set_options_from_form();
543   $locale->set_numberformat_wo_thousands_separator(\%myconfig) if lc($report->{options}->{output_format}) eq 'csv';
544
545   $report->set_columns(%column_defs);
546   $report->set_column_order(@columns);
547
548   $report->set_export_options('generate_report', @hidden_variables, qw(sort revers));
549
550   $report->set_sort_indicator($form->{sort}, $form->{revers} ? 0 : 1);
551
552   CVar->add_custom_variables_to_report('module'         => 'IC',
553                                        'trans_id_field' => 'id',
554                                        'configs'        => $cvar_configs,
555                                        'column_defs'    => \%column_defs,
556                                        'data'           => $form->{parts});
557
558   CVar->add_custom_variables_to_report('module'         => 'IC',
559                                        'sub_module'     => sub { $_[0]->{ioi} },
560                                        'trans_id_field' => 'ioi_id',
561                                        'configs'        => $cvar_configs,
562                                        'column_defs'    => \%column_defs,
563                                        'data'           => $form->{parts});
564
565   my @subtotal_columns = qw(sellprice listprice lastcost);
566   my %subtotals = map { $_ => 0 } ('onhand', @subtotal_columns);
567   my %totals    = map { $_ => 0 } @subtotal_columns;
568   my $idx       = 0;
569   my $same_item = @{ $form->{parts} } ? $form->{parts}[0]{ $form->{sort} } : undef;
570
571   my $defaults  = AM->get_defaults();
572
573   # postprocess parts
574   foreach my $ref (@{ $form->{parts} }) {
575
576     # fresh row, for inserting later
577     my $row = { map { $_ => { 'data' => $ref->{$_} } } @columns };
578
579     $ref->{exchangerate} ||= 1;
580     $ref->{price_factor} ||= 1;
581     $ref->{sellprice}     *= $ref->{exchangerate} / $ref->{price_factor};
582     $ref->{listprice}     *= $ref->{exchangerate} / $ref->{price_factor};
583     $ref->{lastcost}      *= $ref->{exchangerate} / $ref->{price_factor};
584
585     # use this for assemblies
586     my $soldtotal = $bsooqr_mode ? $ref->{soldtotal} : $ref->{onhand};
587
588     if ($ref->{assemblyitem}) {
589       $row->{partnumber}{align}   = 'right';
590       $row->{soldtotal}{data}     = 0;
591       $soldtotal                  = 0 if ($form->{sold});
592     }
593
594     my $edit_link               = build_std_url('script=controller.pl', 'action=Part/edit', 'part.id=' . E($ref->{id}), 'callback');
595     $row->{partnumber}->{link}  = $edit_link;
596     $row->{description}->{link} = $edit_link;
597
598     foreach (qw(sellprice listprice lastcost)) {
599       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{$_}, 2);
600       $row->{"linetotal$_"}{data} = $form->format_amount(\%myconfig, $ref->{onhand} * $ref->{$_}, 2);
601     }
602     foreach ( @pricegroup_columns ) {
603       $row->{$_}{data}            = $form->format_amount(\%myconfig, $ref->{"$_"}, 2);
604     };
605
606
607     map { $row->{$_}{data} = $form->format_amount(\%myconfig, $ref->{$_}); } qw(onhand rop weight soldtotal);
608
609     $row->{weight}->{data} .= ' ' . $defaults->{weightunit};
610
611     # 'yes' and 'no' for boolean value shop
612     if ($form->{l_shop}) {
613       $row->{shop}{data} = $row->{shop}{data}? $::locale->text('yes') : $::locale->text('no');
614     }
615
616     if (!$ref->{assemblyitem}) {
617       foreach my $col (@subtotal_columns) {
618         $totals{$col}    += $soldtotal * $ref->{$col};
619         $subtotals{$col} += $soldtotal * $ref->{$col};
620       }
621
622       $subtotals{soldtotal} += $soldtotal;
623     }
624
625     # set module stuff
626     if ($ref->{module} eq 'oe') {
627       # für oe gibt es vier fälle, jeweils nach kunde oder lieferant unterschiedlich:
628       #
629       # | ist bestellt  | Von Kunden bestellt |  -> edit_oe_ord_link
630       # | Anfrage       | Angebot             |  -> edit_oe_quo_link
631
632       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');
633       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');
634
635       $row->{ordnumber}{link} = $edit_oe_ord_link;
636       $row->{quonumber}{link} = $edit_oe_quo_link if (!$ref->{ordnumber});
637
638     } else {
639       $row->{invnumber}{link} = build_std_url("script=$ref->{module}.pl", 'action=edit', 'type=invoice', 'id=' . E($ref->{trans_id}), 'callback') if ($ref->{invnumber});
640     }
641
642     # set properties of images
643     if ($ref->{image} && (lc $report->{options}->{output_format} eq 'html')) {
644       $row->{image}{data}     = '';
645       $row->{image}{raw_data} = '<a href="' . H($ref->{image}) . '"><img src="' . H($ref->{image}) . '" height="32" border="0"></a>';
646     }
647     map { $row->{$_}{link} = $ref->{$_} } qw(drawing microfiche);
648
649     $row->{notes}{data} = SL::HTML::Util->strip($ref->{notes});
650
651     $report->add_data($row);
652
653     my $next_ref = $form->{parts}[$idx + 1];
654
655     # insert subtotal rows
656     if (($form->{l_subtotal} eq 'Y') &&
657         (!$next_ref ||
658          (!$next_ref->{assemblyitem} && ($same_item ne $next_ref->{ $form->{sort} })))) {
659       my $row = { map { $_ => { 'class' => 'listsubtotal', } } @columns };
660
661       if (($form->{searchitems} ne 'assembly') || !$form->{bom}) {
662         $row->{soldtotal}->{data} = $form->format_amount(\%myconfig, $subtotals{soldtotal});
663       }
664
665       map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $subtotals{$_}, 2) } @subtotal_columns;
666       map { $subtotals{$_} = 0 } ('soldtotal', @subtotal_columns);
667
668       $report->add_data($row);
669
670       $same_item = $next_ref->{ $form->{sort} };
671     }
672
673     $idx++;
674   }
675
676   if ($form->{"l_linetotal"} && !$form->{report_generator_csv_options_for_import}) {
677     my $row = { map { $_ => { 'class' => 'listtotal', } } @columns };
678
679     map { $row->{"linetotal$_"}->{data} = $form->format_amount(\%myconfig, $totals{$_}, 2) } @subtotal_columns;
680
681     $report->add_separator();
682     $report->add_data($row);
683   }
684
685   $report->generate_with_headers();
686
687   $lxdebug->leave_sub();
688 }    #end generate_report
689
690 sub parts_subtotal {
691   $lxdebug->enter_sub();
692
693   $auth->assert('part_service_assembly_edit');
694
695   my (%column_data);
696   my ($column_index, $subtotalonhand, $subtotalsellprice, $subtotallastcost, $subtotallistprice) = @_;
697
698   map { $column_data{$_} = "<td>&nbsp;</td>" } @{ $column_index };
699   $$subtotalonhand = 0 if ($form->{searchitems} eq 'assembly' && $form->{bom});
700
701   $column_data{onhand} =
702       "<th class=listsubtotal align=right>"
703     . $form->format_amount(\%myconfig, $$subtotalonhand)
704     . "</th>";
705
706   $column_data{linetotalsellprice} =
707       "<th class=listsubtotal align=right>"
708     . $form->format_amount(\%myconfig, $$subtotalsellprice, 2)
709     . "</th>";
710   $column_data{linetotallistprice} =
711       "<th class=listsubtotal align=right>"
712     . $form->format_amount(\%myconfig, $$subtotallistprice, 2)
713     . "</th>";
714   $column_data{linetotallastcost} =
715       "<th class=listsubtotal align=right>"
716     . $form->format_amount(\%myconfig, $$subtotallastcost, 2)
717     . "</th>";
718
719   $$subtotalonhand    = 0;
720   $$subtotalsellprice = 0;
721   $$subtotallistprice = 0;
722   $$subtotallastcost  = 0;
723
724   print "<tr class=listsubtotal>";
725
726   map { print "\n$column_data{$_}" } @{ $column_index };
727
728   print qq|
729   </tr>
730 |;
731
732   $lxdebug->leave_sub();
733 }
734
735 sub edit {
736   $lxdebug->enter_sub();
737
738   $auth->assert('part_service_assembly_details');
739
740   # show history button
741   $form->{javascript} = qq|<script type="text/javascript" src="js/show_history.js"></script>|;
742   #/show hhistory button
743   IC->get_part(\%myconfig, \%$form);
744
745   $form->{"original_partnumber"} = $form->{"partnumber"};
746
747   my $title      = 'Edit ' . ucfirst $form->{part_type};
748   $form->{title} = $locale->text($title);
749
750   &link_part;
751   &display_form;
752
753   $lxdebug->leave_sub();
754 }
755
756 sub link_part {
757   $lxdebug->enter_sub();
758
759   $auth->assert('part_service_assembly_details');
760
761   IC->create_links("IC", \%myconfig, \%$form);
762
763   # currencies
764   map({ $form->{selectcurrency} .= "<option>$_\n" } $::form->get_all_currencies());
765
766   # parts and assemblies have the same links
767   my $item = $form->{part_type};
768   if ($form->{part_type} eq 'assembly') {
769     $item = 'part';
770   }
771
772   # build the popup menus
773   $form->{taxaccounts} = "";
774   foreach my $key (keys %{ $form->{IC_links} }) {
775     foreach my $ref (@{ $form->{IC_links}{$key} }) {
776
777       # if this is a tax field
778       if ($key =~ /IC_tax/) {
779         if ($key =~ /\Q$item\E/) {
780           $form->{taxaccounts} .= "$ref->{accno} ";
781           $form->{"IC_tax_$ref->{accno}_description"} =
782             "$ref->{accno}--$ref->{description}";
783
784           if ($form->{id}) {
785             if ($form->{amount}{ $ref->{accno} }) {
786               $form->{"IC_tax_$ref->{accno}"} = "checked";
787             }
788           } else {
789             $form->{"IC_tax_$ref->{accno}"} = "checked";
790           }
791         }
792       } else {
793
794         $form->{"select$key"} .=
795           "<option $ref->{selected}>$ref->{accno}--$ref->{description}\n";
796         if ($form->{amount}{$key} eq $ref->{accno}) {
797           $form->{$key} = "$ref->{accno}--$ref->{description}";
798         }
799
800       }
801     }
802   }
803   chop $form->{taxaccounts};
804
805   if (($form->{part_type} eq "part") || ($form->{part_type} eq "assembly")) {
806     $form->{selectIC_income}  = $form->{selectIC_sale};
807     $form->{selectIC_expense} = $form->{selectIC_cogs};
808     $form->{IC_income}        = $form->{IC_sale};
809     $form->{IC_expense}       = $form->{IC_cogs};
810   }
811
812   delete $form->{IC_links};
813   delete $form->{amount};
814
815   $form->get_partsgroup(\%myconfig, { all => 1 });
816
817   $form->{partsgroup} = "$form->{partsgroup}--$form->{partsgroup_id}";
818
819   if (@{ $form->{all_partsgroup} }) {
820     $form->{selectpartsgroup} = qq|<option>\n|;
821     map { $form->{selectpartsgroup} .= qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n| } @{ $form->{all_partsgroup} };
822   }
823
824   if ($form->{part_type} eq 'assembly') {
825
826     foreach my $i (1 .. $form->{assembly_rows}) {
827       if ($form->{"partsgroup_id_$i"}) {
828         $form->{"partsgroup_$i"} =
829           qq|$form->{"partsgroup_$i"}--$form->{"partsgroup_id_$i"}|;
830       }
831     }
832     $form->get_partsgroup(\%myconfig);
833
834     if (@{ $form->{all_partsgroup} }) {
835       $form->{selectassemblypartsgroup} = qq|<option>\n|;
836
837       map {
838         $form->{selectassemblypartsgroup} .=
839           qq|<option value="$_->{partsgroup}--$_->{id}">$_->{partsgroup}\n|
840       } @{ $form->{all_partsgroup} };
841     }
842   }
843   $lxdebug->leave_sub();
844 }
845
846 sub form_header {
847   $lxdebug->enter_sub();
848
849   $auth->assert('part_service_assembly_details');
850
851   $form->{pg_keys}          = sub { "$_[0]->{partsgroup}--$_[0]->{id}" };
852   $form->{description_area} = ($form->{rows} = $form->numtextrows($form->{description}, 40)) > 1;
853   $form->{notes_rows}       =  max 4, $form->numtextrows($form->{notes}, 40), $form->numtextrows($form->{formel}, 40);
854
855   map { $form->{"is_$_"}  = ($form->{part_type} eq $_) } qw(part service assembly);
856   map { $form->{$_}       =~ s/"/&quot;/g;        } qw(unit);
857
858   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
859                    'partsgroup'    => 'all_partsgroup',
860                    'vendors'       => 'ALL_VENDORS',
861                    'warehouses'    => { 'key'    => 'WAREHOUSES',
862                                         'bins'   => 'BINS', });
863   # leerer wert für Lager und Lagerplatz korrekt einstellt
864   # ID 0 sollte in Ordnung sein, da der Zähler sowieso höher ist
865   my $no_default_bin_entry = { 'id' => '0', description => '--', 'BINS' => [ { id => '0', description => ''} ] };
866   push @ { $form->{WAREHOUSES} }, $no_default_bin_entry;
867   if (my $max = scalar @{ $form->{WAREHOUSES} }) {
868     my ($default_warehouse_id, $default_bin_id);
869     if ($form->{action} eq 'add') { # default only for new entries
870       $default_warehouse_id = $::instance_conf->get_warehouse_id;
871       $default_bin_id       = $::instance_conf->get_bin_id;
872     }
873     $form->{warehouse_id} ||= $default_warehouse_id || $form->{WAREHOUSES}->[$max -1]->{id};
874     $form->{bin_id}       ||= $default_bin_id       ||  $form->{WAREHOUSES}->[$max -1]->{BINS}->[0]->{id};
875   }
876
877   $form->{LANGUAGES}        = SL::DB::Manager::Language->get_all_sorted;
878   $form->{translations_map} = { map { ($_->{language_id} => $_) } @{ $form->{translations} || [] } };
879
880   IC->retrieve_buchungsgruppen(\%myconfig, $form);
881   @{ $form->{BUCHUNGSGRUPPEN} } = grep { $_->{id} eq $form->{buchungsgruppen_id} || ($form->{id} && $form->{orphaned}) || !$form->{id} } @{ $form->{BUCHUNGSGRUPPEN} };
882
883   if (($form->{partnumber} ne '') && !SL::TransNumber->new(number => $form->{partnumber}, type => $form->{part_type}, id => $form->{id})->is_unique) {
884     flash('info', $::locale->text('This partnumber is not unique. You should change it.'));
885   }
886
887   my $units = AM->retrieve_units(\%myconfig, $form);
888   $form->{ALL_UNITS} = [ map +{ name => $_ }, sort { $units->{$a}{sortkey} <=> $units->{$b}{sortkey} } keys %$units ];
889
890   $form->{defaults} = AM->get_defaults();
891
892   $form->{CUSTOM_VARIABLES} = CVar->get_custom_variables('module' => 'IC', 'trans_id' => $form->{id});
893
894   my ($null, $partsgroup_id) = split /--/, $form->{partsgroup};
895
896   CVar->render_inputs('variables' => $form->{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $partsgroup_id)
897     if (scalar @{ $form->{CUSTOM_VARIABLES} });
898
899   $::request->layout->use_javascript("${_}.js") for qw(ckeditor/ckeditor ckeditor/adapters/jquery kivi.PriceRule);
900   $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $::form->{id} * 1 ]})});") if $::form->{id};
901   $form->header;
902   #print $form->parse_html_template('ic/form_header', { ALL_PRICE_FACTORS => $form->{ALL_PRICE_FACTORS},
903   #                                                     ALL_UNITS         => $form->{ALL_UNITS},
904   #                                                     BUCHUNGSGRUPPEN   => $form->{BUCHUNGSGRUPPEN},
905   #                                                     payment_terms     => $form->{payment_terms},
906   #                                                     all_partsgroup    => $form->{all_partsgroup}});
907
908   $form->{show_edit_buttons} = $main::auth->check_right($::myconfig{login}, 'part_service_assembly_edit');
909
910   print $form->parse_html_template('ic/form_header');
911   $lxdebug->leave_sub();
912 }
913
914 sub form_footer {
915   $lxdebug->enter_sub();
916
917   $auth->assert('part_service_assembly_details');
918
919   print $form->parse_html_template('ic/form_footer');
920
921   $lxdebug->leave_sub();
922 }
923
924 sub makemodel_row {
925   $lxdebug->enter_sub();
926   my ($numrows) = @_;
927   #hli
928   my @mm_data = grep { any { $_ ne '' } @$_{qw(make model)} } map +{ make => $form->{"make_$_"}, model => $form->{"model_$_"}, lastcost => $form->{"lastcost_$_"}, lastupdate => $form->{"lastupdate_$_"}, sortorder => $form->{"sortorder_$_"} }, 1 .. $numrows;
929   delete @{$form}{grep { m/^make_\d+/ || m/^model_\d+/ } keys %{ $form }};
930   print $form->parse_html_template('ic/makemodel', { MM_DATA => [ @mm_data, {} ], mm_rows => scalar @mm_data + 1 });
931
932   $lxdebug->leave_sub();
933 }
934
935 sub assembly_row {
936   $lxdebug->enter_sub();
937   my ($numrows) = @_;
938   my (@column_index);
939   my ($nochange, $callback, $previousform, $linetotal, $line_purchase_price, $href);
940
941   @column_index = qw(runningnumber qty unit bom partnumber description partsgroup lastcost total);
942
943   if ($form->{previousform}) {
944     $nochange     = 1;
945     @column_index = qw(qty unit bom partnumber description partsgroup total);
946   } else {
947
948     # change callback
949     $form->{old_callback} = $form->{callback};
950     $callback             = $form->{callback};
951     $form->{callback}     = "$form->{script}?action=display_form";
952
953     # delete action
954     map { delete $form->{$_} } qw(action header);
955
956     # save form variables in a previousform variable
957     my %form_to_save = map   { ($_ => m/^ (?: listprice | sellprice | lastcost ) $/x ? $form->format_amount(\%myconfig, $form->{$_}) : $form->{$_}) }
958                        keys %{ $form };
959     $previousform    = $::auth->save_form_in_session(form => \%form_to_save);
960
961     $form->{callback} = $callback;
962     $form->{assemblytotal} = 0;
963     $form->{assembly_purchase_price_total} = 0;
964     $form->{weight}        = 0;
965   }
966
967   my %header = (
968    runningnumber => { text =>  $locale->text('No.'),              nowrap => 1, width => '5%',  align => 'left',},
969    qty           => { text =>  $locale->text('Qty'),              nowrap => 1, width => '10%', align => 'left',},
970    unit          => { text =>  $locale->text('Unit'),             nowrap => 1, width => '5%',  align => 'left',},
971    partnumber    => { text =>  $locale->text('Part Number'),      nowrap => 1, width => '20%', align => 'left',},
972    description   => { text =>  $locale->text('Part Description'), nowrap => 1, width => '50%', align => 'left',},
973    lastcost      => { text =>  $locale->text('Purchase Prices'),  nowrap => 1, width => '50%', align => 'right',},
974    total         => { text =>  $locale->text('Sale Prices'),      nowrap => 1,                 align => 'right',},
975    bom           => { text =>  $locale->text('BOM'),                                           align => 'center',},
976    partsgroup    => { text =>  $locale->text('Group'),                                         align => 'left',},
977   );
978
979   my @ROWS;
980
981   for my $i (1 .. $numrows) {
982     my (%row, @row_hiddens);
983
984     $form->{"partnumber_$i"} =~ s/\"/&quot;/g;
985
986     $linetotal           = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
987     $line_purchase_price = $form->round_amount($form->{"lastcost_$i"} *  $form->{"qty_$i"} / ($form->{"price_factor_$i"} || 1), 4);
988     $form->{assemblytotal}                  += $linetotal;
989     $form->{assembly_purchase_price_total}  += $line_purchase_price;
990     $form->{"qty_$i"}    = $form->format_amount(\%myconfig, $form->{"qty_$i"});
991     $linetotal           = $form->format_amount(\%myconfig, $linetotal, 2);
992     $line_purchase_price = $form->format_amount(\%myconfig, $line_purchase_price, 2);
993     $href                = build_std_url("action=edit", qq|id=$form->{"id_$i"}|, "rowcount=$numrows", "currow=$i", "previousform=$previousform");
994     map { $row{$_}{data} = "" } qw(qty unit partnumber description bom partsgroup runningnumber);
995
996     # last row
997     if (($i >= 1) && ($i == $numrows)) {
998       if (!$form->{previousform}) {
999         $row{partnumber}{data}  = qq|<input name="partnumber_$i" size=15 value="$form->{"partnumber_$i"}">|;
1000         $row{qty}{data}         = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
1001         $row{description}{data} = qq|<input name="description_$i" size=40 value="$form->{"description_$i"}">|;
1002         $row{partsgroup}{data}  = qq|<input name="partsgroup_$i" size=10 value="$form->{"partsgroup_$i"}">|;
1003       }
1004     # other rows
1005     } else {
1006       if ($form->{previousform}) {
1007         push @row_hiddens,          qw(qty bom);
1008         $row{partnumber}{data}    = $form->{"partnumber_$i"};
1009         $row{qty}{data}           = $form->{"qty_$i"};
1010         $row{bom}{data}           = $form->{"bom_$i"} ? "x" : "&nbsp;";
1011         $row{qty}{align}          = 'right';
1012       } else {
1013         $row{partnumber}{data}    = qq|$form->{"partnumber_$i"}|;
1014         $row{partnumber}{link}     = $href;
1015         $row{qty}{data}           = qq|<input name="qty_$i" size=5 value="$form->{"qty_$i"}">|;
1016         $row{runningnumber}{data} = qq|<input name="runningnumber_$i" size=3 value="$i">|;
1017         $row{bom}{data}   = sprintf qq|<input name="bom_$i" type=checkbox class=checkbox value=1 %s>|,
1018                                        $form->{"bom_$i"} ? 'checked' : '';
1019       }
1020       push @row_hiddens,        qw(unit description partnumber partsgroup);
1021       $row{unit}{data}        = $form->{"unit_$i"};
1022       #Bei der Artikelbeschreibung und Warengruppe können Sonderzeichen verwendet
1023       #werden, die den HTML Code stören. Daher sollen diese im Template escaped werden
1024       #dies geschieht, wenn die Variable escape gesetzt ist
1025       $row{description}{data}   = $form->{"description_$i"};
1026       $row{description}{escape} = 1;
1027       $row{partsgroup}{data}    = $form->{"partsgroup_$i"};
1028       $row{partsgroup}{escape}  = 1;
1029       $row{bom}{align}          = 'center';
1030     }
1031
1032     $row{lastcost}{data}      = $line_purchase_price;
1033     $row{total}{data}         = $linetotal;
1034     $row{lastcost}{align}     = 'right';
1035     $row{total}{align}        = 'right';
1036     $row{deliverydate}{align} = 'right';
1037
1038     push @row_hiddens, qw(id sellprice lastcost weight price_factor_id price_factor);
1039     $row{hiddens} = [ map +{ name => "${_}_$i", value => $form->{"${_}_$i"} }, @row_hiddens ];
1040
1041     push @ROWS, \%row;
1042   }
1043
1044   print $form->parse_html_template('ic/assembly_row', { COLUMNS => \@column_index, ROWS => \@ROWS, HEADER => \%header });
1045
1046   $lxdebug->leave_sub();
1047 }
1048
1049 sub update {
1050   $lxdebug->enter_sub();
1051
1052   $auth->assert('part_service_assembly_edit');
1053
1054   # update checks whether pricegroups, makemodels or assembly items have been changed/added
1055   # new items might have been added (and the original form might have been stored and restored)
1056   # so at the end the ic form is run through check_form in io.pl
1057   # The various combination of events can lead to problems with the order of parse_amount and format_amount
1058   # Currently check_form parses some variables in assembly mode, but not in article or service mode
1059   # This will only ever really be sanely resolved with a rewrite...
1060
1061   # parse pricegroups. and no, don't rely on check_form for this...
1062   map { $form->{"price_$_"} = $form->parse_amount(\%myconfig, $form->{"price_$_"}) } 1 .. $form->{price_rows};
1063
1064   unless ($form->{part_type} eq 'assembly') {
1065     # for assemblies check_form will parse sellprice and listprice, but not for parts or services
1066     $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(sellprice listprice ve gv);
1067   };
1068
1069   if ($form->{part_type} eq 'part') {
1070     $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) for qw(weight rop);
1071   }
1072
1073   # same for makemodel lastcosts
1074   # but parse_amount not necessary for assembly component lastcosts
1075   unless ($form->{part_type} eq "assembly") {
1076     map { $form->{"lastcost_$_"} = $form->parse_amount(\%myconfig, $form->{"lastcost_$_"}) } 1 .. $form->{"makemodel_rows"};
1077     $form->{lastcost} = $form->parse_amount(\%myconfig, $form->{lastcost});
1078   }
1079
1080   if ($form->{part_type} eq "assembly") {
1081     my $i = $form->{assembly_rows};
1082
1083     # if last row is empty check the form otherwise retrieve item
1084     if (   ($form->{"partnumber_$i"} eq "")
1085         && ($form->{"description_$i"} eq "")
1086         && ($form->{"partsgroup_$i"}  eq "")) {
1087       # no new assembly item was added
1088
1089       &check_form;
1090
1091     } else {
1092       # search db for newly added assemblyitems, via partnumber or description
1093       IC->assembly_item(\%myconfig, \%$form);
1094
1095       # form->{item_list} contains the possible matches, next check whether the
1096       # match is unique or we need to call the page to select the item
1097       my $rows = scalar @{ $form->{item_list} };
1098
1099       if ($rows) {
1100         $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"});
1101
1102         if ($rows > 1) {
1103           $form->{makemodel_rows}--;
1104           select_item(mode => 'IC', pre_entered_qty => $form->parse_amount(\%myconfig, $form->{"qty_$i"}));
1105           $::dispatcher->end_request;
1106         } else {
1107           map { $form->{item_list}[$i]{$_} =~ s/\"/&quot;/g }
1108             qw(partnumber description unit partsgroup);
1109           map { $form->{"${_}_$i"} = $form->{item_list}[0]{$_} }
1110             keys %{ $form->{item_list}[0] };
1111           $form->{"runningnumber_$i"} = $form->{assembly_rows};
1112           $form->{assembly_rows}++;
1113
1114           &check_form;
1115
1116         }
1117
1118       } else {
1119
1120         $form->{rowcount} = $i;
1121         $form->{assembly_rows}++;
1122
1123         &new_item;
1124
1125       }
1126     }
1127
1128   } elsif (($form->{part_type} eq 'part') || ($form->{part_type} eq 'service')) {
1129     &check_form;
1130   }
1131
1132   $lxdebug->leave_sub();
1133 }
1134
1135 sub save {
1136   $lxdebug->enter_sub();
1137
1138   $auth->assert('part_service_assembly_edit');
1139   $::form->mtime_ischanged('parts');
1140   my ($parts_id, %newform, $amount, $callback);
1141
1142   # check if there is a part number - commented out, cause there is an automatic allocation of numbers
1143   # $form->isblank("partnumber", $locale->text(ucfirst $form->{part_type}." Part Number missing!"));
1144
1145   # check if there is a description
1146   $form->isblank("description", $locale->text("Part Description missing!"));
1147
1148   $form->error($locale->text("Inventory quantity must be zero before you can set this $form->{part_type} obsolete!"))
1149     if $form->{obsolete} && $form->{onhand} * 1 && $form->{part_type} ne 'service';
1150
1151   if (!$form->{buchungsgruppen_id}) {
1152     $form->error($locale->text("Parts must have an entry type.") . " " .
1153      $locale->text("If you see this message, you most likely just setup your LX-Office and haven't added any entry types. If this is the case, the option is accessible for administrators in the System menu.")
1154     );
1155   }
1156
1157   $form->error($locale->text('Description must not be empty!')) unless $form->{description};
1158   $form->error($locale->text('Partnumber must not be set to empty!')) if $form->{id} && !$form->{partnumber};
1159
1160   # undef warehouse_id if the empty value is selected
1161   if ( ($form->{warehouse_id} == 0) && ($form->{bin_id} == 0) ) {
1162     undef $form->{warehouse_id};
1163     undef $form->{bin_id};
1164   }
1165   # save part
1166   if (IC->save(\%myconfig, \%$form) == 3) {
1167     $form->error($locale->text('Partnumber not unique!'));
1168   }
1169   # saving the history
1170   if(!exists $form->{addition}) {
1171     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1172     $form->{what_done} = "part";
1173     $form->{addition}  = "SAVED";
1174     $form->save_history;
1175   }
1176   # /saving the history
1177   $parts_id = $form->{id};
1178
1179   my $i;
1180   # load previous variables
1181   if ($form->{previousform}) {
1182
1183     # save the new form variables before splitting previousform
1184     map { $newform{$_} = $form->{$_} } keys %$form;
1185
1186     # don't trample on previous variables
1187     map { delete $form->{$_} } keys %newform;
1188
1189     my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1190     my @ic_cvar_fields  = map { "cvar_$_->{name}" } @{ $ic_cvar_configs };
1191
1192     # restore original values
1193     $::auth->restore_form_from_session($newform{previousform}, form => $form);
1194     $form->{taxaccounts} = $newform{taxaccount2};
1195
1196     if ($form->{part_type} eq 'assembly') {
1197
1198       # undo number formatting
1199       map { $form->{$_} = $form->parse_amount(\%myconfig, $form->{$_}) }
1200         qw(weight listprice sellprice rop);
1201
1202       $form->{assembly_rows}--;
1203       if ($newform{currow}) {
1204         $i = $newform{currow};
1205       } else {
1206         $i = $form->{assembly_rows};
1207       }
1208       $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
1209
1210       $form->{sellprice} -= $form->{"sellprice_$i"} * $form->{"qty_$i"};
1211       $form->{weight}    -= $form->{"weight_$i"} * $form->{"qty_$i"};
1212
1213       # change/add values for assembly item
1214       map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit weight listprice sellprice inventory_accno income_accno expense_accno price_factor_id);
1215       map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
1216
1217       # das ist __voll__ bekloppt, dass so auszurechnen jb 22.5.09
1218       #$form->{sellprice} += $form->{"sellprice_$i"} * $form->{"qty_$i"};
1219       $form->{weight}    += $form->{"weight_$i"} * $form->{"qty_$i"};
1220
1221     } else {
1222
1223       # set values for last invoice/order item
1224       $i = $form->{rowcount};
1225       $form->{"qty_$i"} = 1 unless ($form->{"qty_$i"} > 0);
1226
1227       map { $form->{"${_}_$i"} = $newform{$_} } qw(partnumber description bin unit listprice inventory_accno income_accno expense_accno sellprice lastcost price_factor_id);
1228       map { $form->{"ic_${_}_$i"} = $newform{$_} } @ic_cvar_fields;
1229
1230       $form->{"longdescription_$i"} = $newform{notes};
1231
1232       $form->{"sellprice_$i"} = $newform{lastcost} if ($form->{vendor_id});
1233
1234       if ($form->{exchangerate} != 0) {
1235         $form->{"sellprice_$i"} /= $form->{exchangerate};
1236       }
1237
1238       map { $form->{"taxaccounts_$i"} .= "$_ " } split / /, $newform{taxaccount};
1239       chop $form->{"taxaccounts_$i"};
1240       foreach my $item (qw(description rate taxnumber)) {
1241         my $index = $form->{"taxaccounts_$i"} . "_$item";
1242         $form->{$index} = $newform{$index};
1243       }
1244
1245       # credit remaining calculation
1246       $amount = $form->{"sellprice_$i"} * (1 - $form->{"discount_$i"} / 100) * $form->{"qty_$i"};
1247
1248       map { $form->{"${_}_base"} += $amount } (split / /, $form->{"taxaccounts_$i"});
1249       map { $amount += ($form->{"${_}_base"} * $form->{"${_}_rate"}) } split / /, $form->{"taxaccounts_$i"} if !$form->{taxincluded};
1250
1251       $form->{creditremaining} -= $amount;
1252
1253       # redo number formatting, because invoice parse them!
1254       map { $form->{"${_}_$i"} = $form->format_amount(\%myconfig, $form->{"${_}_$i"}) } qw(weight listprice sellprice lastcost rop);
1255     }
1256
1257     $form->{"id_$i"} = $parts_id;
1258
1259     # Get the actual price factor (not just the ID) for the marge calculation.
1260     $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
1261     foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1262       next if ($pfac->{id} != $newform{price_factor_id});
1263       $form->{"marge_price_factor_$i"} = $pfac->{factor};
1264       last;
1265     }
1266     delete $form->{ALL_PRICE_FACTORS};
1267
1268     delete $form->{action};
1269
1270     # restore original callback
1271     $callback = $form->unescape($form->{callback});
1272     $form->{callback} = $form->unescape($form->{old_callback});
1273     delete $form->{old_callback};
1274
1275     $form->{makemodel_rows}--;
1276
1277     # put callback together
1278     foreach my $key (keys %$form) {
1279
1280       # do single escape for Apache 2.0
1281       my $value = $form->escape($form->{$key}, 1);
1282       $callback .= qq|&$key=$value|;
1283     }
1284     $form->{callback} = $callback;
1285   }
1286
1287   # redirect
1288   $form->redirect;
1289
1290   $lxdebug->leave_sub();
1291 }
1292
1293 sub save_as_new {
1294   $lxdebug->enter_sub();
1295
1296   $auth->assert('part_service_assembly_edit');
1297
1298   # saving the history
1299   if(!exists $form->{addition}) {
1300     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1301     $form->{addition}  = "SAVED AS NEW";
1302     $form->{what_done} = "part";
1303     $form->save_history;
1304   }
1305   # /saving the history
1306
1307   # deleting addition to get the history saved for the new part, too.
1308   delete $form->{addition};
1309
1310   $form->{id} = 0;
1311   if ($form->{"original_partnumber"} &&
1312       ($form->{"partnumber"} eq $form->{"original_partnumber"})) {
1313     $form->{partnumber} = "";
1314   }
1315   &save;
1316   $lxdebug->leave_sub();
1317 }
1318
1319 sub delete {
1320   $lxdebug->enter_sub();
1321
1322   $auth->assert('part_service_assembly_edit');
1323
1324   # saving the history
1325   if(!exists $form->{addition}) {
1326     $form->{snumbers}  = qq|partnumber_| . $form->{partnumber};
1327     $form->{addition}  = "DELETED";
1328     $form->{what_done} = "part";
1329     $form->save_history;
1330   }
1331   # /saving the history
1332   my $rc = IC->delete(\%myconfig, \%$form);
1333
1334   # redirect
1335   $form->redirect($locale->text('Item deleted!')) if ($rc > 0);
1336   $form->error($locale->text('Cannot delete item!'));
1337
1338   $lxdebug->leave_sub();
1339 }
1340
1341 sub price_row {
1342   $lxdebug->enter_sub();
1343
1344   $auth->assert('part_service_assembly_details');
1345
1346   my ($numrows) = @_;
1347
1348   my @PRICES = map +{
1349     pricegroup    => $form->{"pricegroup_$_"},
1350     pricegroup_id => $form->{"pricegroup_id_$_"},
1351     price         => $form->{"price_$_"},
1352   }, 1 .. $numrows;
1353
1354   print $form->parse_html_template('ic/price_row', { PRICES => \@PRICES });
1355
1356   $lxdebug->leave_sub();
1357 }
1358
1359 sub ajax_autocomplete {
1360   $main::lxdebug->enter_sub();
1361
1362   my $form     = $main::form;
1363   my %myconfig = %main::myconfig;
1364
1365   $form->{column}          = 'description'     unless $form->{column} =~ /^partnumber|description$/;
1366   $form->{$form->{column}} = $form->{q}           || '';
1367   $form->{limit}           = ($form->{limit} * 1) || 10;
1368   $form->{searchitems}   ||= '';
1369
1370   my @results = IC->all_parts(\%myconfig, $form);
1371
1372   print $form->ajax_response_header(),
1373         $form->parse_html_template('ic/ajax_autocomplete');
1374
1375   $main::lxdebug->leave_sub();
1376 }
1377
1378 sub display_form {
1379   $::lxdebug->enter_sub;
1380
1381   $auth->assert('part_service_assembly_edit');
1382
1383   relink_accounts();
1384
1385   $::form->language_payment(\%::myconfig);
1386
1387   Common::webdav_folder($::form);
1388
1389   form_header();
1390   price_row($::form->{price_rows});
1391   makemodel_row(++$::form->{makemodel_rows}) if $::form->{part_type} =~ /^(part|service)$/;
1392   assembly_row(++$::form->{assembly_rows})   if $::form->{part_type} eq 'assembly';
1393
1394   form_footer();
1395
1396   $::lxdebug->leave_sub;
1397 }
1398
1399 sub back_to_record {
1400   _check_io_auth();
1401
1402
1403   delete @{$::form}{qw(action action_add action_back_to_record back_sub description item notes partnumber sellprice taxaccount2 unit vc)};
1404
1405   $::auth->restore_form_from_session($::form->{previousform}, clobber => 1);
1406   $::form->{rowcount}--;
1407   $::form->{action}   = 'display_form';
1408   $::form->{callback} = $::form->{script} . '?' . join('&', map { $::form->escape($_) . '=' . $::form->escape($::form->{$_}) } sort keys %{ $::form });
1409   $::form->redirect;
1410 }
1411
1412 sub continue { call_sub($form->{"nextsub"}); }
1413
1414 sub dispatcher {
1415   my $action = first { $::form->{"action_${_}"} } qw(add back_to_record);
1416   $::form->error($::locale->text('No action defined.')) unless $action;
1417
1418   $::form->{dispatched_action} = $action;
1419   call_sub($action);
1420 }