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