694732e1df3395342a3764e158186b0d2ec3839f
[kivitendo-erp.git] / SL / OE.pm
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) 1999-2003
10 #
11 #  Author: Dieter Simader
12 #   Email: dsimader@sql-ledger.org
13 #     Web: http://www.sql-ledger.org
14 #
15 #  Contributors:
16 #
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; either version 2 of the License, or
20 # (at your option) any later version.
21 #
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 # GNU General Public License for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
29 #======================================================================
30 #
31 # Order entry module
32 # Quotation
33 #======================================================================
34
35 package OE;
36
37 use List::Util qw(max first);
38 use YAML;
39
40 use SL::AM;
41 use SL::Common;
42 use SL::CVar;
43 use SL::DB::Order;
44 use SL::DB::PeriodicInvoicesConfig;
45 use SL::DB::Status;
46 use SL::DBUtils;
47 use SL::IC;
48
49 use strict;
50
51 sub transactions {
52   $main::lxdebug->enter_sub();
53
54   my ($self, $myconfig, $form) = @_;
55
56   # connect to database
57   my $dbh = $form->get_standard_dbh;
58
59   my $query;
60   my $ordnumber = 'ordnumber';
61   my $quotation = '0';
62
63   my @values;
64   my $where;
65
66   my ($periodic_invoices_columns, $periodic_invoices_joins);
67
68   my $rate = ($form->{vc} eq 'customer') ? 'buy' : 'sell';
69
70   if ($form->{type} =~ /_quotation$/) {
71     $quotation = '1';
72     $ordnumber = 'quonumber';
73
74   } elsif ($form->{type} eq 'sales_order') {
75     $periodic_invoices_columns = qq| , COALESCE(pcfg.active, 'f') AS periodic_invoices |;
76     $periodic_invoices_joins   = qq| LEFT JOIN periodic_invoices_configs pcfg ON (o.id = pcfg.oe_id) |;
77   }
78
79   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
80
81   $query =
82     qq|SELECT o.id, o.ordnumber, o.transdate, o.reqdate, | .
83     qq|  o.amount, ct.${vc}number, ct.name, o.netamount, o.${vc}_id, o.globalproject_id, | .
84     qq|  o.closed, o.delivered, o.quonumber, o.shippingpoint, o.shipvia, | .
85     qq|  o.transaction_description, | .
86     qq|  o.marge_total, o.marge_percent, | .
87     qq|  ex.$rate AS exchangerate, | .
88     qq|  pr.projectnumber AS globalprojectnumber, | .
89     qq|  e.name AS employee, s.name AS salesman, | .
90     qq|  ct.${vc}number AS vcnumber, ct.country, ct.ustid  | .
91     $periodic_invoices_columns .
92     qq|FROM oe o | .
93     qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
94     qq|LEFT JOIN employee e ON (o.employee_id = e.id) | .
95     qq|LEFT JOIN employee s ON (o.salesman_id = s.id) | .
96     qq|LEFT JOIN exchangerate ex ON (ex.currency_id = o.currency_id | .
97     qq|  AND ex.transdate = o.transdate) | .
98     qq|LEFT JOIN project pr ON (o.globalproject_id = pr.id) | .
99     qq|$periodic_invoices_joins | .
100     qq|WHERE (o.quotation = ?) |;
101   push(@values, $quotation);
102
103   my ($null, $split_department_id) = split /--/, $form->{department};
104   my $department_id = $form->{department_id} || $split_department_id;
105   if ($department_id) {
106     $query .= qq| AND o.department_id = ?|;
107     push(@values, $department_id);
108   }
109
110   if ($form->{"project_id"}) {
111     $query .=
112       qq|AND ((globalproject_id = ?) OR EXISTS | .
113       qq|  (SELECT * FROM orderitems oi | .
114       qq|   WHERE oi.project_id = ? AND oi.trans_id = o.id))|;
115     push(@values, conv_i($form->{"project_id"}), conv_i($form->{"project_id"}));
116   }
117
118   if ($form->{"projectnumber"}) {
119     $query .= <<SQL;
120       AND ((pr.projectnumber ILIKE ?) OR EXISTS (
121         SELECT * FROM orderitems oi
122         LEFT JOIN project proi ON proi.id = oi.project_id
123         WHERE proi.projectnumber ILIKE ? AND oi.trans_id = o.id
124       ))
125 SQL
126     push @values, "%" . $form->{"projectnumber"} . "%", "%" . $form->{"projectnumber"} . "%" ;
127   }
128
129   if ($form->{"${vc}_id"}) {
130     $query .= " AND o.${vc}_id = ?";
131     push(@values, $form->{"${vc}_id"});
132
133   } elsif ($form->{$vc}) {
134     $query .= " AND ct.name ILIKE ?";
135     push(@values, '%' . $form->{$vc} . '%');
136   }
137
138   if (!$main::auth->assert('sales_all_edit', 1)) {
139     $query .= " AND o.employee_id = (select id from employee where login= ?)";
140     push @values, $form->{login};
141   }
142   if ($form->{employee_id}) {
143     $query .= " AND o.employee_id = ?";
144     push @values, conv_i($form->{employee_id});
145   }
146
147   if ($form->{salesman_id}) {
148     $query .= " AND o.salesman_id = ?";
149     push @values, conv_i($form->{salesman_id});
150   }
151
152   if (!$form->{open} && !$form->{closed}) {
153     $query .= " AND o.id = 0";
154   } elsif (!($form->{open} && $form->{closed})) {
155     $query .= ($form->{open}) ? " AND o.closed = '0'" : " AND o.closed = '1'";
156   }
157
158   if (($form->{"notdelivered"} || $form->{"delivered"}) &&
159       ($form->{"notdelivered"} ne $form->{"delivered"})) {
160     $query .= $form->{"delivered"} ?
161       " AND o.delivered " : " AND NOT o.delivered";
162   }
163
164   if ($form->{$ordnumber}) {
165     $query .= qq| AND o.$ordnumber ILIKE ?|;
166     push(@values, '%' . $form->{$ordnumber} . '%');
167   }
168
169   if($form->{transdatefrom}) {
170     $query .= qq| AND o.transdate >= ?|;
171     push(@values, conv_date($form->{transdatefrom}));
172   }
173
174   if($form->{transdateto}) {
175     $query .= qq| AND o.transdate <= ?|;
176     push(@values, conv_date($form->{transdateto}));
177   }
178
179   if($form->{reqdatefrom}) {
180     $query .= qq| AND o.reqdate >= ?|;
181     push(@values, conv_date($form->{reqdatefrom}));
182   }
183
184   if($form->{reqdateto}) {
185     $query .= qq| AND o.reqdate <= ?|;
186     push(@values, conv_date($form->{reqdateto}));
187   }
188
189   if ($form->{transaction_description}) {
190     $query .= qq| AND o.transaction_description ILIKE ?|;
191     push(@values, '%' . $form->{transaction_description} . '%');
192   }
193
194   if ($form->{periodic_invoices_active} ne $form->{periodic_invoices_inactive}) {
195     my $not  = $form->{periodic_invoices_inactive} ? 'NOT' : '';
196     $query  .= qq| AND ${not} COALESCE(pcfg.active, 'f')|;
197   }
198
199   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
200   my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name")));
201   my %allowed_sort_columns = (
202     "transdate"               => "o.transdate",
203     "reqdate"                 => "o.reqdate",
204     "id"                      => "o.id",
205     "ordnumber"               => "o.ordnumber",
206     "quonumber"               => "o.quonumber",
207     "name"                    => "ct.name",
208     "employee"                => "e.name",
209     "salesman"                => "s.name",
210     "shipvia"                 => "o.shipvia",
211     "transaction_description" => "o.transaction_description"
212   );
213   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
214     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}";
215   }
216   $query .= qq| ORDER by | . $sortorder;
217
218   my $sth = $dbh->prepare($query);
219   $sth->execute(@values) ||
220     $form->dberror($query . " (" . join(", ", @values) . ")");
221
222   my %id = ();
223   $form->{OE} = [];
224   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
225     $ref->{exchangerate} = 1 unless $ref->{exchangerate};
226     push @{ $form->{OE} }, $ref if $ref->{id} != $id{ $ref->{id} };
227     $id{ $ref->{id} } = $ref->{id};
228   }
229
230   $sth->finish;
231
232   $main::lxdebug->leave_sub();
233 }
234
235 sub transactions_for_todo_list {
236   $main::lxdebug->enter_sub();
237
238   my $self     = shift;
239   my %params   = @_;
240
241   my $myconfig = \%main::myconfig;
242   my $form     = $main::form;
243
244   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
245
246   my $query    = qq|SELECT id FROM employee WHERE login = ?|;
247   my ($e_id)   = selectrow_query($form, $dbh, $query, $form->{login});
248
249   $query       =
250     qq|SELECT oe.id, oe.transdate, oe.reqdate, oe.quonumber, oe.transaction_description, oe.amount,
251          CASE WHEN (COALESCE(oe.customer_id, 0) = 0) THEN 'vendor' ELSE 'customer' END AS vc,
252          c.name AS customer,
253          v.name AS vendor,
254          e.name AS employee
255        FROM oe
256        LEFT JOIN customer c ON (oe.customer_id = c.id)
257        LEFT JOIN vendor v   ON (oe.vendor_id   = v.id)
258        LEFT JOIN employee e ON (oe.employee_id = e.id)
259        WHERE (COALESCE(quotation, FALSE) = TRUE)
260          AND (COALESCE(closed,    FALSE) = FALSE)
261          AND ((oe.employee_id = ?) OR (oe.salesman_id = ?))
262          AND NOT (oe.reqdate ISNULL)
263          AND (oe.reqdate < current_date)
264        ORDER BY transdate|;
265
266   my $quotations = selectall_hashref_query($form, $dbh, $query, $e_id, $e_id);
267
268   $main::lxdebug->leave_sub();
269
270   return $quotations;
271 }
272
273 sub save {
274   $main::lxdebug->enter_sub();
275
276   my ($self, $myconfig, $form) = @_;
277
278   # connect to database, turn off autocommit
279   my $dbh = $form->get_standard_dbh;
280
281   my ($query, @values, $sth, $null);
282   my $exchangerate = 0;
283
284   my $all_units = AM->retrieve_units($myconfig, $form);
285   $form->{all_units} = $all_units;
286
287   my $ic_cvar_configs = CVar->get_configs(module => 'IC',
288                                           dbh    => $dbh);
289
290   $form->{employee_id} = (split /--/, $form->{employee})[1] if !$form->{employee_id};
291   unless ($form->{employee_id}) {
292     $form->get_employee($dbh);
293   }
294
295   my $ml = ($form->{type} eq 'sales_order') ? 1 : -1;
296
297   if ($form->{id}) {
298     $query = qq|DELETE FROM custom_variables
299                 WHERE (config_id IN (SELECT id FROM custom_variable_configs WHERE module = 'IC'))
300                   AND (sub_module = 'orderitems')
301                   AND (trans_id IN (SELECT id FROM orderitems WHERE trans_id = ?))|;
302     do_query($form, $dbh, $query, $form->{id});
303
304     $query = qq|DELETE FROM orderitems WHERE trans_id = ?|;
305     do_query($form, $dbh, $query, $form->{id});
306
307     $query = qq|DELETE FROM shipto | .
308              qq|WHERE trans_id = ? AND module = 'OE'|;
309     do_query($form, $dbh, $query, $form->{id});
310
311   } else {
312
313     $query = qq|SELECT nextval('id')|;
314     ($form->{id}) = selectrow_query($form, $dbh, $query);
315
316     $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults))|;
317     do_query($form, $dbh, $query, $form->{id}, $form->{employee_id});
318   }
319
320   my $amount    = 0;
321   my $linetotal = 0;
322   my $discount  = 0;
323   my $project_id;
324   my $reqdate;
325   my $taxrate;
326   my $taxbase;
327   my $taxdiff;
328   my $taxamount = 0;
329   my $fxsellprice;
330   my %taxbase;
331   my @taxaccounts;
332   my %taxaccounts;
333   my $netamount = 0;
334
335   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
336   my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
337   my $price_factor;
338
339   for my $i (1 .. $form->{rowcount}) {
340
341     map({ $form->{"${_}_$i"} = $form->parse_amount($myconfig, $form->{"${_}_$i"}) } qw(qty ship));
342
343     if ($form->{"id_$i"}) {
344
345       # get item baseunit
346       $query = qq|SELECT unit FROM parts WHERE id = ?|;
347       my ($item_unit) = selectrow_query($form, $dbh, $query, $form->{"id_$i"});
348
349       my $basefactor = 1;
350       if (defined($all_units->{$item_unit}->{factor}) &&
351           (($all_units->{$item_unit}->{factor} * 1) != 0)) {
352         $basefactor = $all_units->{$form->{"unit_$i"}}->{factor} / $all_units->{$item_unit}->{factor};
353       }
354       my $baseqty = $form->{"qty_$i"} * $basefactor;
355
356       $form->{"marge_percent_$i"} = $form->parse_amount($myconfig, $form->{"marge_percent_$i"}) * 1;
357       $form->{"marge_absolut_$i"} = $form->parse_amount($myconfig, $form->{"marge_absolut_$i"}) * 1;
358
359       $form->{"lastcost_$i"} = $form->parse_amount($myconfig, $form->{"lastcost_$i"});
360
361       # set values to 0 if nothing entered
362       $form->{"discount_$i"} = $form->parse_amount($myconfig, $form->{"discount_$i"}) / 100;
363
364       $form->{"sellprice_$i"} = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
365       $fxsellprice = $form->{"sellprice_$i"};
366
367       my ($dec) = ($form->{"sellprice_$i"} =~ /\.(\d+)/);
368       $dec = length($dec);
369       my $decimalplaces = ($dec > 2) ? $dec : 2;
370
371       $discount = $form->round_amount($form->{"sellprice_$i"} * $form->{"discount_$i"}, $decimalplaces);
372       $form->{"sellprice_$i"} = $form->round_amount($form->{"sellprice_$i"} - $discount, $decimalplaces);
373
374       $form->{"inventory_accno_$i"} *= 1;
375       $form->{"expense_accno_$i"}   *= 1;
376
377       $price_factor = $price_factors{ $form->{"price_factor_id_$i"} } || 1;
378       $linetotal    = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor, 2);
379
380       @taxaccounts = split(/ /, $form->{"taxaccounts_$i"});
381       $taxrate     = 0;
382       $taxdiff     = 0;
383
384       map { $taxrate += $form->{"${_}_rate"} } @taxaccounts;
385
386       if ($form->{taxincluded}) {
387         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
388         $taxbase   = $linetotal - $taxamount;
389
390         # we are not keeping a natural price, do not round
391         $form->{"sellprice_$i"} =
392           $form->{"sellprice_$i"} * (1 / (1 + $taxrate));
393       } else {
394         $taxamount = $linetotal * $taxrate;
395         $taxbase   = $linetotal;
396       }
397
398       if ($form->round_amount($taxrate, 7) == 0) {
399         if ($form->{taxincluded}) {
400           foreach my $item (@taxaccounts) {
401             $taxamount = $form->round_amount($linetotal * $form->{"${item}_rate"} / (1 + abs($form->{"${item}_rate"})), 2);
402             $taxaccounts{$item} += $taxamount;
403             $taxdiff            += $taxamount;
404             $taxbase{$item}     += $taxbase;
405           }
406           $taxaccounts{ $taxaccounts[0] } += $taxdiff;
407         } else {
408           foreach my $item (@taxaccounts) {
409             $taxaccounts{$item} += $linetotal * $form->{"${item}_rate"};
410             $taxbase{$item}     += $taxbase;
411           }
412         }
413       } else {
414         foreach my $item (@taxaccounts) {
415           $taxaccounts{$item} += $taxamount * $form->{"${item}_rate"} / $taxrate;
416           $taxbase{$item} += $taxbase;
417         }
418       }
419
420       $netamount += $form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor;
421
422       $reqdate = ($form->{"reqdate_$i"}) ? $form->{"reqdate_$i"} : undef;
423
424       # Get pricegroup_id and save it. Unfortunately the interface
425       # also uses ID "0" for signalling that none is selected, but "0"
426       # must not be stored in the database. Therefore we cannot simply
427       # use conv_i().
428       ($null, my $pricegroup_id) = split(/--/, $form->{"sellprice_pg_$i"});
429       $pricegroup_id *= 1;
430       $pricegroup_id  = undef if !$pricegroup_id;
431
432       # save detail record in orderitems table
433       my $orderitems_id = $form->{"orderitems_id_$i"};
434       ($orderitems_id)  = selectfirst_array_query($form, $dbh, qq|SELECT nextval('orderitemsid')|) if (!$orderitems_id);
435
436       @values = ();
437       $query = qq|INSERT INTO orderitems (
438                     id, trans_id, parts_id, description, longdescription, qty, base_qty,
439                     sellprice, discount, unit, reqdate, project_id, serialnumber, ship,
440                     pricegroup_id, ordnumber, transdate, cusordnumber, subtotal,
441                     marge_percent, marge_total, lastcost, price_factor_id, price_factor, marge_price_factor)
442                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
443                           (SELECT factor FROM price_factors WHERE id = ?), ?)|;
444       push(@values,
445            conv_i($orderitems_id), conv_i($form->{id}), conv_i($form->{"id_$i"}),
446            $form->{"description_$i"}, $form->{"longdescription_$i"},
447            $form->{"qty_$i"}, $baseqty,
448            $fxsellprice, $form->{"discount_$i"},
449            $form->{"unit_$i"}, conv_date($reqdate), conv_i($form->{"project_id_$i"}),
450            $form->{"serialnumber_$i"}, $form->{"ship_$i"}, $pricegroup_id,
451            $form->{"ordnumber_$i"}, conv_date($form->{"transdate_$i"}),
452            $form->{"cusordnumber_$i"}, $form->{"subtotal_$i"} ? 't' : 'f',
453            $form->{"marge_percent_$i"}, $form->{"marge_absolut_$i"},
454            $form->{"lastcost_$i"},
455            conv_i($form->{"price_factor_id_$i"}), conv_i($form->{"price_factor_id_$i"}),
456            conv_i($form->{"marge_price_factor_$i"}));
457       do_query($form, $dbh, $query, @values);
458
459       $form->{"sellprice_$i"} = $fxsellprice;
460       $form->{"discount_$i"} *= 100;
461
462       CVar->save_custom_variables(module       => 'IC',
463                                   sub_module   => 'orderitems',
464                                   trans_id     => $orderitems_id,
465                                   configs      => $ic_cvar_configs,
466                                   variables    => $form,
467                                   name_prefix  => 'ic_',
468                                   name_postfix => "_$i",
469                                   dbh          => $dbh);
470     }
471   }
472
473   $reqdate = ($form->{reqdate}) ? $form->{reqdate} : undef;
474
475   # add up the tax
476   my $tax = 0;
477   map { $tax += $form->round_amount($taxaccounts{$_}, 2) } keys %taxaccounts;
478
479   $amount = $form->round_amount($netamount + $tax, 2);
480   $netamount = $form->round_amount($netamount, 2);
481
482   if ($form->{currency} eq $form->{defaultcurrency}) {
483     $form->{exchangerate} = 1;
484   } else {
485     $exchangerate = $form->check_exchangerate($myconfig, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? 'buy' : 'sell');
486   }
487
488   $form->{exchangerate} = $exchangerate || $form->parse_amount($myconfig, $form->{exchangerate});
489
490   my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
491
492   ($null, $form->{department_id}) = split(/--/, $form->{department}) if $form->{department};
493
494   # save OE record
495   $query =
496     qq|UPDATE oe SET
497          ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
498          customer_id = ?, amount = ?, netamount = ?, reqdate = ?, taxincluded = ?,
499          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?,
500          delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
501          taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,
502          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
503        WHERE id = ?|;
504
505   @values = ($form->{ordnumber} || '', $form->{quonumber},
506              $form->{cusordnumber}, conv_date($form->{transdate}),
507              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
508              $amount, $netamount, conv_date($reqdate),
509              $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint},
510              $form->{shipvia}, $form->{notes}, $form->{intnotes},
511              $form->{currency}, $form->{closed} ? 't' : 'f',
512              $form->{delivered} ? "t" : "f", $form->{proforma} ? 't' : 'f',
513              $quotation, conv_i($form->{department_id}),
514              conv_i($form->{language_id}), conv_i($form->{taxzone_id}),
515              conv_i($form->{shipto_id}), conv_i($form->{payment_id}),
516              conv_i($form->{delivery_vendor_id}),
517              conv_i($form->{delivery_customer_id}),
518              conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
519              conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
520              $form->{transaction_description},
521              $form->{marge_total} * 1, $form->{marge_percent} * 1,
522              conv_i($form->{id}));
523   do_query($form, $dbh, $query, @values);
524
525   $form->{ordtotal} = $amount;
526
527   # add shipto
528   $form->{name} = $form->{ $form->{vc} };
529   $form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//;
530
531   if (!$form->{shipto_id}) {
532     $form->add_shipto($dbh, $form->{id}, "OE");
533   }
534
535   # save printed, emailed, queued
536   $form->save_status($dbh);
537
538   # Link this record to the records it was created from.
539   $form->{convert_from_oe_ids} =~ s/^\s+//;
540   $form->{convert_from_oe_ids} =~ s/\s+$//;
541   my @convert_from_oe_ids      =  split m/\s+/, $form->{convert_from_oe_ids};
542   delete $form->{convert_from_oe_ids};
543
544   if (scalar @convert_from_oe_ids) {
545     RecordLinks->create_links('dbh'        => $dbh,
546                               'mode'       => 'ids',
547                               'from_table' => 'oe',
548                               'from_ids'   => \@convert_from_oe_ids,
549                               'to_table'   => 'oe',
550                               'to_id'      => $form->{id},
551       );
552
553     $self->_close_quotations_rfqs('dbh'     => $dbh,
554                                   'from_id' => \@convert_from_oe_ids,
555                                   'to_id'   => $form->{id});
556   }
557
558   if (($form->{currency} ne $form->{defaultcurrency}) && !$exchangerate) {
559     if ($form->{vc} eq 'customer') {
560       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, $form->{exchangerate}, 0);
561     }
562     if ($form->{vc} eq 'vendor') {
563       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, 0, $form->{exchangerate});
564     }
565   }
566
567   $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ?
568                                        "quonumber" : "ordnumber"};
569
570   Common::webdav_folder($form);
571
572   my $rc = $dbh->commit;
573
574   $self->save_periodic_invoices_config(dbh         => $dbh,
575                                        oe_id       => $form->{id},
576                                        config_yaml => $form->{periodic_invoices_config})
577     if ($form->{type} eq 'sales_order');
578
579   $main::lxdebug->leave_sub();
580
581   return $rc;
582 }
583
584 sub save_periodic_invoices_config {
585   my ($self, %params) = @_;
586
587   return if !$params{oe_id};
588
589   my $config = $params{config_yaml} ? YAML::Load($params{config_yaml}) : undef;
590   return if 'HASH' ne ref $config;
591
592   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
593           || SL::DB::PeriodicInvoicesConfig->new(oe_id => $params{oe_id});
594   $obj->update_attributes(%{ $config });
595 }
596
597 sub load_periodic_invoice_config {
598   my $self = shift;
599   my $form = shift;
600
601   delete $form->{periodic_invoices_config};
602
603   if ($form->{id}) {
604     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
605
606     if ($config_obj) {
607       my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity start_date_as_date end_date_as_date extend_automatically_by ar_chart_id
608                                                       print printer_id copies) };
609       $form->{periodic_invoices_config} = YAML::Dump($config);
610     }
611   }
612 }
613
614 sub _close_quotations_rfqs {
615   $main::lxdebug->enter_sub();
616
617   my $self     = shift;
618   my %params   = @_;
619
620   Common::check_params(\%params, qw(from_id to_id));
621
622   my $myconfig = \%main::myconfig;
623   my $form     = $main::form;
624
625   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
626
627   my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
628   my $sth      = prepare_query($form, $dbh, $query);
629
630   do_statement($form, $sth, $query, conv_i($params{to_id}));
631
632   my ($quotation) = $sth->fetchrow_array();
633
634   if ($quotation) {
635     $main::lxdebug->leave_sub();
636     return;
637   }
638
639   my @close_ids;
640
641   foreach my $from_id (@{ $params{from_id} }) {
642     $from_id = conv_i($from_id);
643     do_statement($form, $sth, $query, $from_id);
644     ($quotation) = $sth->fetchrow_array();
645     push @close_ids, $from_id if ($quotation);
646   }
647
648   $sth->finish();
649
650   if (scalar @close_ids) {
651     $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
652     do_query($form, $dbh, $query, @close_ids);
653
654     $dbh->commit() unless ($params{dbh});
655   }
656
657   $main::lxdebug->leave_sub();
658 }
659
660 sub delete {
661   $main::lxdebug->enter_sub();
662
663   my ($self, $myconfig, $form) = @_;
664
665   my $rc = SL::DB::Order->new->db->with_transaction(sub {
666     my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $form->{id} ]) };
667
668     SL::DB::Order->new(id => $form->{id})->delete;
669
670     my $spool = $::lx_office_conf{paths}->{spool};
671     unlink map { "$spool/$_" } @spoolfiles if $spool;
672
673     1;
674   });
675
676   $main::lxdebug->leave_sub();
677
678   return $rc;
679 }
680
681 sub retrieve {
682   $main::lxdebug->enter_sub();
683
684   my ($self, $myconfig, $form) = @_;
685
686   # connect to database
687   my $dbh = $form->get_standard_dbh;
688
689   my ($query, $query_add, @values, @ids, $sth);
690
691   # translate the ids (given by id_# and trans_id_#) into one array of ids, so we can join them later
692   map {
693     push @ids, $form->{"trans_id_$_"}
694       if ($form->{"multi_id_$_"} and $form->{"trans_id_$_"})
695   } (1 .. $form->{"rowcount"});
696
697   if ($form->{rowcount} && scalar @ids) {
698     $form->{convert_from_oe_ids} = join ' ', @ids;
699   }
700
701   # if called in multi id mode, and still only got one id, switch back to single id
702   if ($form->{"rowcount"} and $#ids == 0) {
703     $form->{"id"} = $ids[0];
704     undef @ids;
705   }
706
707   # and remember for the rest of the function
708   my $is_collective_order = scalar @ids;
709
710   if (!$form->{id}) {
711     my $wday         = (localtime(time))[6];
712     my $next_workday = $wday == 5 ? 3 : $wday == 6 ? 2 : 1;
713     $query_add       = qq|, current_date AS transdate, date(current_date + interval '${next_workday} days') AS reqdate|;
714   }
715
716   # get default accounts
717   $query = qq|SELECT (SELECT c.accno FROM chart c WHERE d.inventory_accno_id = c.id) AS inventory_accno,
718                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
719                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
720                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
721                      (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno
722               $query_add
723               FROM defaults d|;
724   my $ref = selectfirst_hashref_query($form, $dbh, $query);
725   map { $form->{$_} = $ref->{$_} } keys %$ref;
726
727   $form->{currency} = $form->get_default_currency($myconfig);
728
729   # set reqdate if this is an invoice->order conversion. If someone knows a better check to ensure
730   # we come from invoices, feel free.
731   $form->{reqdate} = $form->{deliverydate}
732     if (    $form->{deliverydate}
733         and $form->{callback} =~ /action=ar_transactions/);
734
735   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
736
737   if ($form->{id} or @ids) {
738
739     # retrieve order for single id
740     # NOTE: this query is intended to fetch all information only ONCE.
741     # so if any of these infos is important (or even different) for any item,
742     # it will be killed out and then has to be fetched from the item scope query further down
743     $query =
744       qq|SELECT o.cp_id, o.ordnumber, o.transdate, o.reqdate,
745            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
746            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
747            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
748            o.closed, o.reqdate, o.quonumber, o.department_id, o.cusordnumber,
749            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
750            o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
751            o.globalproject_id, o.delivered, o.transaction_description
752          FROM oe o
753          JOIN ${vc} cv ON (o.${vc}_id = cv.id)
754          LEFT JOIN employee e ON (o.employee_id = e.id)
755          LEFT JOIN department d ON (o.department_id = d.id) | .
756         ($form->{id}
757          ? "WHERE o.id = ?"
758          : "WHERE o.id IN (" . join(', ', map("? ", @ids)) . ")"
759         );
760     @values = $form->{id} ? ($form->{id}) : @ids;
761     $sth = prepare_execute_query($form, $dbh, $query, @values);
762
763     $ref = $sth->fetchrow_hashref("NAME_lc");
764
765     if ($ref) {
766       map { $form->{$_} = $ref->{$_} } keys %$ref;
767
768       $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ? "quonumber" : "ordnumber"};
769
770       # set all entries for multiple ids blank that yield different information
771       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
772         map { $form->{$_} = '' if ($ref->{$_} ne $form->{$_}) } keys %$ref;
773       }
774     }
775
776     # if not given, fill transdate with current_date
777     $form->{transdate} = $form->current_date($myconfig)
778       unless $form->{transdate};
779
780     $sth->finish;
781
782     if ($form->{delivery_customer_id}) {
783       $query = qq|SELECT name FROM customer WHERE id = ?|;
784       ($form->{delivery_customer_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_customer_id});
785     }
786
787     if ($form->{delivery_vendor_id}) {
788       $query = qq|SELECT name FROM customer WHERE id = ?|;
789       ($form->{delivery_vendor_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_vendor_id});
790     }
791
792     # shipto and pinted/mailed/queued status makes only sense for single id retrieve
793     if (!@ids) {
794       $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'OE'|;
795       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
796
797       $ref = $sth->fetchrow_hashref("NAME_lc");
798       delete($ref->{id});
799       map { $form->{$_} = $ref->{$_} } keys %$ref;
800       $sth->finish;
801
802       # get printed, emailed and queued
803       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
804       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
805
806       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
807         $form->{printed} .= "$ref->{formname} " if $ref->{printed};
808         $form->{emailed} .= "$ref->{formname} " if $ref->{emailed};
809         $form->{queued}  .= "$ref->{formname} $ref->{spoolfile} " if $ref->{spoolfile};
810       }
811       $sth->finish;
812       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
813     }    # if !@ids
814
815     my $transdate = $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
816
817     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
818
819     # retrieve individual items
820     # this query looks up all information about the items
821     # stuff different from the whole will not be overwritten, but saved with a suffix.
822     $query =
823       qq|SELECT o.id AS orderitems_id,
824            c1.accno AS inventory_accno, c1.new_chart_id AS inventory_new_chart, date($transdate) - c1.valid_from as inventory_valid,
825            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
826            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
827            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
828            p.partnumber, p.assembly, p.listprice, o.description, o.qty,
829            o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.inventory_accno_id AS part_inventory_accno_id,
830            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
831            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
832            o.price_factor_id, o.price_factor, o.marge_price_factor,
833            pr.projectnumber, p.formel,
834            pg.partsgroup, o.pricegroup_id, (SELECT pricegroup FROM pricegroup WHERE id=o.pricegroup_id) as pricegroup
835          FROM orderitems o
836          JOIN parts p ON (o.parts_id = p.id)
837          JOIN oe ON (o.trans_id = oe.id)
838          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
839          LEFT JOIN chart c2 ON ((SELECT income_accno_id_$form->{taxzone_id}  FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c2.id)
840          LEFT JOIN chart c3 ON ((SELECT expense_accno_id_$form->{taxzone_id} FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c3.id)
841          LEFT JOIN project pr ON (o.project_id = pr.id)
842          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
843       ($form->{id}
844        ? qq|WHERE o.trans_id = ?|
845        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
846       qq|ORDER BY o.oid|;
847
848     @ids = $form->{id} ? ($form->{id}) : @ids;
849     $sth = prepare_execute_query($form, $dbh, $query, @values);
850
851     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
852       # Retrieve custom variables.
853       my $cvars = CVar->get_custom_variables(dbh        => $dbh,
854                                              module     => 'IC',
855                                              sub_module => 'orderitems',
856                                              trans_id   => $ref->{orderitems_id},
857                                             );
858       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
859
860       # Handle accounts.
861       if (!$ref->{"part_inventory_accno_id"}) {
862         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
863       }
864       delete($ref->{"part_inventory_accno_id"});
865
866       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
867       #   unless already present there
868       # remove _oe entries afterwards
869       map { $ref->{$_} = $ref->{"${_}_oe"} if ($ref->{$_} eq '') }
870         qw|ordnumber transdate cusordnumber|
871         if (@ids);
872       map { delete $ref->{$_} } qw|ordnumber_oe transdate_oe cusordnumber_oe|;
873
874
875
876       while ($ref->{inventory_new_chart} && ($ref->{inventory_valid} >= 0)) {
877         my $query =
878           qq|SELECT accno AS inventory_accno, | .
879           qq|  new_chart_id AS inventory_new_chart, | .
880           qq|  date($transdate) - valid_from AS inventory_valid | .
881           qq|FROM chart WHERE id = $ref->{inventory_new_chart}|;
882         ($ref->{inventory_accno}, $ref->{inventory_new_chart},
883          $ref->{inventory_valid}) = selectrow_query($form, $dbh, $query);
884       }
885
886       while ($ref->{income_new_chart} && ($ref->{income_valid} >= 0)) {
887         my $query =
888           qq|SELECT accno AS income_accno, | .
889           qq|  new_chart_id AS income_new_chart, | .
890           qq|  date($transdate) - valid_from AS income_valid | .
891           qq|FROM chart WHERE id = $ref->{income_new_chart}|;
892         ($ref->{income_accno}, $ref->{income_new_chart},
893          $ref->{income_valid}) = selectrow_query($form, $dbh, $query);
894       }
895
896       while ($ref->{expense_new_chart} && ($ref->{expense_valid} >= 0)) {
897         my $query =
898           qq|SELECT accno AS expense_accno, | .
899           qq|  new_chart_id AS expense_new_chart, | .
900           qq|  date($transdate) - valid_from AS expense_valid | .
901           qq|FROM chart WHERE id = $ref->{expense_new_chart}|;
902         ($ref->{expense_accno}, $ref->{expense_new_chart},
903          $ref->{expense_valid}) = selectrow_query($form, $dbh, $query);
904       }
905
906       # delete orderitems_id in collective orders, so that they get cloned no matter what
907       delete $ref->{orderitems_id} if (@ids);
908
909       # get tax rates and description
910       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
911       $query =
912         qq|SELECT c.accno, t.taxdescription, t.rate, t.taxnumber | .
913         qq|FROM tax t LEFT JOIN chart c on (c.id = t.chart_id) | .
914         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
915         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
916         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
917         qq|ORDER BY c.accno|;
918       my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
919       $ref->{taxaccounts} = "";
920       my $i = 0;
921       while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
922         if (($ptr->{accno} eq "") && ($ptr->{rate} == 0)) {
923           $i++;
924           $ptr->{accno} = $i;
925         }
926         $ref->{taxaccounts} .= "$ptr->{accno} ";
927         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
928           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
929           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
930           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
931           $form->{taxaccounts} .= "$ptr->{accno} ";
932         }
933
934       }
935
936       chop $ref->{taxaccounts};
937
938       push @{ $form->{form_details} }, $ref;
939       $stw->finish;
940     }
941     $sth->finish;
942
943   } else {
944
945     # get last name used
946     $form->lastname_used($dbh, $myconfig, $form->{vc})
947       unless $form->{"$form->{vc}_id"};
948
949   }
950
951   $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? "buy" : "sell");
952
953   Common::webdav_folder($form);
954
955   $self->load_periodic_invoice_config($form);
956
957   my $rc = $dbh->commit;
958
959   $main::lxdebug->leave_sub();
960
961   return $rc;
962 }
963
964 sub retrieve_simple {
965   $main::lxdebug->enter_sub();
966
967   my $self     = shift;
968   my %params   = @_;
969
970   Common::check_params(\%params, qw(id));
971
972   my $myconfig    = \%main::myconfig;
973   my $form        = $main::form;
974
975   my $dbh         = $params{dbh} || $form->get_standard_dbh($myconfig);
976
977   my $oe_query    = qq|SELECT * FROM oe         WHERE id = ?|;
978   my $oi_query    = qq|SELECT * FROM orderitems WHERE trans_id = ?|;
979
980   my $order            = selectfirst_hashref_query($form, $dbh, $oe_query, conv_i($params{id}));
981   $order->{orderitems} = selectall_hashref_query(  $form, $dbh, $oi_query, conv_i($params{id}));
982
983   $main::lxdebug->leave_sub();
984
985   return $order;
986 }
987
988 sub order_details {
989   $main::lxdebug->enter_sub();
990
991   my ($self, $myconfig, $form) = @_;
992
993   # connect to database
994   my $dbh = $form->get_standard_dbh;
995   my $query;
996   my @values = ();
997   my $sth;
998   my $nodiscount;
999   my $yesdiscount;
1000   my $nodiscount_subtotal = 0;
1001   my $discount_subtotal = 0;
1002   my $item;
1003   my $i;
1004   my @partsgroup = ();
1005   my $partsgroup;
1006   my $position = 0;
1007   my $subtotal_header = 0;
1008   my $subposition = 0;
1009   my %taxaccounts;
1010   my %taxbase;
1011   my $tax_rate;
1012   my $taxamount;
1013
1014   my (@project_ids, %projectnumbers, %projectdescriptions);
1015
1016   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
1017
1018   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS',
1019                    'departments'   => 'ALL_DEPARTMENTS');
1020   my %price_factors;
1021
1022   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1023     $price_factors{$pfac->{id}}  = $pfac;
1024     $pfac->{factor}             *= 1;
1025     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
1026   }
1027
1028   # lookup department
1029   foreach my $dept (@{ $form->{ALL_DEPARTMENTS} }) {
1030     next unless $dept->{id} eq $form->{department_id};
1031     $form->{department} = $dept->{description};
1032     last;
1033   }
1034
1035   # sort items by partsgroup
1036   for $i (1 .. $form->{rowcount}) {
1037     $partsgroup = "";
1038     if ($form->{"partsgroup_$i"} && $form->{groupitems}) {
1039       $partsgroup = $form->{"partsgroup_$i"};
1040     }
1041     push @partsgroup, [$i, $partsgroup];
1042     push(@project_ids, $form->{"project_id_$i"}) if ($form->{"project_id_$i"});
1043   }
1044
1045   if (@project_ids) {
1046     $query = "SELECT id, projectnumber, description FROM project WHERE id IN (" .
1047       join(", ", map("?", @project_ids)) . ")";
1048     $sth = prepare_execute_query($form, $dbh, $query, @project_ids);
1049     while (my $ref = $sth->fetchrow_hashref()) {
1050       $projectnumbers{$ref->{id}} = $ref->{projectnumber};
1051       $projectdescriptions{$ref->{id}} = $ref->{description};
1052     }
1053     $sth->finish();
1054   }
1055
1056   $form->{"globalprojectnumber"} = $projectnumbers{$form->{"globalproject_id"}};
1057   $form->{"globalprojectdescription"} = $projectdescriptions{$form->{"globalproject_id"}};
1058
1059   $form->{discount} = [];
1060
1061   $form->{TEMPLATE_ARRAYS} = { };
1062   IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
1063
1064   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1065
1066   my @arrays =
1067     qw(runningnumber number description longdescription qty ship unit bin
1068        partnotes serialnumber reqdate sellprice listprice netprice
1069        discount p_discount discount_sub nodiscount_sub
1070        linetotal  nodiscount_linetotal tax_rate projectnumber projectdescription
1071        price_factor price_factor_name partsgroup weight lineweight);
1072
1073   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
1074
1075   my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
1076
1077   map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays);
1078
1079   my $totalweight = 0;
1080   my $sameitem = "";
1081   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
1082     $i = $item->[0];
1083
1084     if ($item->[1] ne $sameitem) {
1085       push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, qq|$item->[1]|);
1086       $sameitem = $item->[1];
1087
1088       map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1089     }
1090
1091     $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
1092
1093     if ($form->{"id_$i"} != 0) {
1094
1095       # add number, description and qty to $form->{number}, ....
1096
1097       if ($form->{"subtotal_$i"} && !$subtotal_header) {
1098         $subtotal_header = $i;
1099         $position = int($position);
1100         $subposition = 0;
1101         $position++;
1102       } elsif ($subtotal_header) {
1103         $subposition += 1;
1104         $position = int($position);
1105         $position = $position.".".$subposition;
1106       } else {
1107         $position = int($position);
1108         $position++;
1109       }
1110
1111       my $price_factor = $price_factors{$form->{"price_factor_id_$i"}} || { 'factor' => 1 };
1112
1113       push @{ $form->{TEMPLATE_ARRAYS}->{runningnumber} },     $position;
1114       push @{ $form->{TEMPLATE_ARRAYS}->{number} },            $form->{"partnumber_$i"};
1115       push @{ $form->{TEMPLATE_ARRAYS}->{description} },       $form->{"description_$i"};
1116       push @{ $form->{TEMPLATE_ARRAYS}->{longdescription} },   $form->{"longdescription_$i"};
1117       push @{ $form->{TEMPLATE_ARRAYS}->{qty} },               $form->format_amount($myconfig, $form->{"qty_$i"});
1118       push @{ $form->{TEMPLATE_ARRAYS}->{qty_nofmt} },         $form->{"qty_$i"};
1119       push @{ $form->{TEMPLATE_ARRAYS}->{ship} },              $form->format_amount($myconfig, $form->{"ship_$i"});
1120       push @{ $form->{TEMPLATE_ARRAYS}->{ship_nofmt} },        $form->{"ship_$i"};
1121       push @{ $form->{TEMPLATE_ARRAYS}->{unit} },              $form->{"unit_$i"};
1122       push @{ $form->{TEMPLATE_ARRAYS}->{bin} },               $form->{"bin_$i"};
1123       push @{ $form->{TEMPLATE_ARRAYS}->{partnotes} },         $form->{"partnotes_$i"};
1124       push @{ $form->{TEMPLATE_ARRAYS}->{serialnumber} },      $form->{"serialnumber_$i"};
1125       push @{ $form->{TEMPLATE_ARRAYS}->{reqdate} },           $form->{"reqdate_$i"};
1126       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
1127       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1128       push @{ $form->{TEMPLATE_ARRAYS}->{listprice} },         $form->{"listprice_$i"};
1129       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
1130       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
1131       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
1132
1133       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1134       my ($dec)         = ($sellprice =~ /\.(\d+)/);
1135       my $decimalplaces = max 2, length($dec);
1136
1137       my $parsed_discount            = $form->parse_amount($myconfig, $form->{"discount_$i"});
1138
1139       my $linetotal_exact            = $form->{"qty_$i"} * $sellprice * (100 - $parsed_discount) / 100 / $price_factor->{factor};
1140       my $linetotal                  = $form->round_amount($linetotal_exact, 2);
1141
1142       my $nodiscount_exact_linetotal = $form->{"qty_$i"} * $sellprice                                  / $price_factor->{factor};
1143       my $nodiscount_linetotal       = $form->round_amount($nodiscount_exact_linetotal,2);
1144
1145       my $discount                   = $nodiscount_linetotal - $linetotal; # is always rounded because $nodiscount_linetotal and $linetotal are rounded
1146
1147       my $discount_round_error       = $discount + ($linetotal_exact - $nodiscount_exact_linetotal); # not used
1148
1149       $form->{"netprice_$i"}   = $form->round_amount($form->{"qty_$i"} ? ($linetotal / $form->{"qty_$i"}) : 0, 2);
1150
1151       push @{ $form->{TEMPLATE_ARRAYS}->{netprice} },       ($form->{"netprice_$i"} != 0) ? $form->format_amount($myconfig, $form->{"netprice_$i"}, $decimalplaces) : '';
1152       push @{ $form->{TEMPLATE_ARRAYS}->{netprice_nofmt} }, ($form->{"netprice_$i"} != 0) ? $form->{"netprice_$i"} : '';
1153
1154       $linetotal = ($linetotal != 0) ? $linetotal : '';
1155
1156       push @{ $form->{TEMPLATE_ARRAYS}->{discount} },       ($discount != 0) ? $form->format_amount($myconfig, $discount * -1, 2) : '';
1157       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
1158       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
1159
1160       $form->{ordtotal}         += $linetotal;
1161       $form->{nodiscount_total} += $nodiscount_linetotal;
1162       $form->{discount_total}   += $discount;
1163
1164       if ($subtotal_header) {
1165         $discount_subtotal   += $linetotal;
1166         $nodiscount_subtotal += $nodiscount_linetotal;
1167       }
1168
1169       if ($form->{"subtotal_$i"} && $subtotal_header && ($subtotal_header != $i)) {
1170         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub} },         $form->format_amount($myconfig, $discount_subtotal,   2);
1171         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub_nofmt} },   $discount_subtotal;
1172         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub} },       $form->format_amount($myconfig, $nodiscount_subtotal, 2);
1173         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub_nofmt} }, $nodiscount_subtotal;
1174
1175         $discount_subtotal   = 0;
1176         $nodiscount_subtotal = 0;
1177         $subtotal_header     = 0;
1178
1179       } else {
1180         push @{ $form->{TEMPLATE_ARRAYS}->{$_} }, "" for qw(discount_sub nodiscount_sub discount_sub_nofmt nodiscount_sub_nofmt);
1181       }
1182
1183       if (!$form->{"discount_$i"}) {
1184         $nodiscount += $linetotal;
1185       }
1186
1187       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal} },                  $form->format_amount($myconfig, $linetotal, 2);
1188       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal_nofmt} },            $linetotal_exact;
1189       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal} },       $form->format_amount($myconfig, $nodiscount_linetotal, 2);
1190       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal_nofmt} }, $nodiscount_linetotal;
1191       push(@{ $form->{TEMPLATE_ARRAYS}->{projectnumber} },              $projectnumbers{$form->{"project_id_$i"}});
1192       push(@{ $form->{TEMPLATE_ARRAYS}->{projectdescription} },         $projectdescriptions{$form->{"project_id_$i"}});
1193
1194       my $lineweight = $form->{"qty_$i"} * $form->{"weight_$i"};
1195       $totalweight += $lineweight;
1196       push @{ $form->{TEMPLATE_ARRAYS}->{weight} },            $form->format_amount($myconfig, $form->{"weight_$i"}, 3);
1197       push @{ $form->{TEMPLATE_ARRAYS}->{weight_nofmt} },      $form->{"weight_$i"};
1198       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight} },        $form->format_amount($myconfig, $lineweight, 3);
1199       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight_nofmt} },  $lineweight;
1200
1201       my ($taxamount, $taxbase);
1202       my $taxrate = 0;
1203
1204       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
1205
1206       if ($form->{taxincluded}) {
1207
1208         # calculate tax
1209         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
1210         $taxbase = $linetotal / (1 + $taxrate);
1211       } else {
1212         $taxamount = $linetotal * $taxrate;
1213         $taxbase   = $linetotal;
1214       }
1215
1216       if ($taxamount != 0) {
1217         foreach my $accno (split / /, $form->{"taxaccounts_$i"}) {
1218           $taxaccounts{$accno} += $taxamount * $form->{"${accno}_rate"} / $taxrate;
1219           $taxbase{$accno}     += $taxbase;
1220         }
1221       }
1222
1223       $tax_rate = $taxrate * 100;
1224       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
1225
1226       if ($form->{"assembly_$i"}) {
1227         $sameitem = "";
1228
1229         # get parts and push them onto the stack
1230         my $sortorder = "";
1231         if ($form->{groupitems}) {
1232           $sortorder = qq|ORDER BY pg.partsgroup, a.oid|;
1233         } else {
1234           $sortorder = qq|ORDER BY a.oid|;
1235         }
1236
1237         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
1238                  qq|pg.partsgroup | .
1239                  qq|FROM assembly a | .
1240                  qq|  JOIN parts p ON (a.parts_id = p.id) | .
1241                  qq|    LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1242                  qq|    WHERE a.bom = '1' | .
1243                  qq|    AND a.id = ? | . $sortorder;
1244         @values = ($form->{"id_$i"});
1245         $sth = $dbh->prepare($query);
1246         $sth->execute(@values) || $form->dberror($query);
1247
1248         while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1249           if ($form->{groupitems} && $ref->{partsgroup} ne $sameitem) {
1250             map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1251             $sameitem = ($ref->{partsgroup}) ? $ref->{partsgroup} : "--";
1252             push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $sameitem);
1253           }
1254
1255           push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $form->format_amount($myconfig, $ref->{qty} * $form->{"qty_$i"}) . qq|, $ref->{partnumber}, $ref->{description}|);
1256           map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } @arrays));
1257         }
1258         $sth->finish;
1259       }
1260
1261       push @{ $form->{TEMPLATE_ARRAYS}->{"ic_cvar_$_->{name}"} },
1262         CVar->format_to_template(CVar->parse($form->{"ic_cvar_$_->{name}_$i"}, $_), $_)
1263           for @{ $ic_cvar_configs };
1264     }
1265   }
1266
1267   $form->{totalweight}       = $form->format_amount($myconfig, $totalweight, 3);
1268   $form->{totalweight_nofmt} = $totalweight;
1269   my $defaults = AM->get_defaults();
1270   $form->{weightunit}        = $defaults->{weightunit};
1271
1272   my $tax = 0;
1273   foreach $item (sort keys %taxaccounts) {
1274     $tax += $taxamount = $form->round_amount($taxaccounts{$item}, 2);
1275
1276     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase} },        $form->format_amount($myconfig, $taxbase{$item}, 2));
1277     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase_nofmt} },  $taxbase{$item});
1278     push(@{ $form->{TEMPLATE_ARRAYS}->{tax} },            $form->format_amount($myconfig, $taxamount,      2));
1279     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_nofmt} },      $taxamount);
1280     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
1281     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
1282     push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $form->{"${item}_description"} . q{ } . 100 * $form->{"${item}_rate"} . q{%});
1283     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
1284   }
1285
1286   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
1287   $form->{discount_total}      = $form->format_amount($myconfig, $form->{discount_total}, 2);
1288   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
1289   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
1290
1291   if($form->{taxincluded}) {
1292     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal} - $tax, 2);
1293     $form->{subtotal_nofmt} = $form->{ordtotal} - $tax;
1294   } else {
1295     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1296     $form->{subtotal_nofmt} = $form->{ordtotal};
1297   }
1298
1299   $form->{ordtotal} = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
1300
1301   # format amounts
1302   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1303
1304   if ($form->{type} =~ /_quotation/) {
1305     $form->set_payment_options($myconfig, $form->{quodate});
1306   } else {
1307     $form->set_payment_options($myconfig, $form->{orddate});
1308   }
1309
1310   $form->{username} = $myconfig->{name};
1311
1312   $dbh->disconnect;
1313
1314   $main::lxdebug->leave_sub();
1315 }
1316
1317 sub project_description {
1318   $main::lxdebug->enter_sub();
1319
1320   my ($self, $dbh, $id) = @_;
1321
1322   my $query = qq|SELECT description FROM project WHERE id = ?|;
1323   my ($value) = selectrow_query($main::form, $dbh, $query, $id);
1324
1325   $main::lxdebug->leave_sub();
1326
1327   return $value;
1328 }
1329
1330 1;
1331
1332 __END__
1333
1334 =head1 NAME
1335
1336 OE.pm - Order entry module
1337
1338 =head1 DESCRIPTION
1339
1340 OE.pm is part of the OE module. OE is responsible for sales and purchase orders, as well as sales quotations and purchase requests. This file abstracts the database tables C<oe> and C<orderitems>.
1341
1342 =head1 FUNCTIONS
1343
1344 =over 4
1345
1346 =item retrieve_simple PARAMS
1347
1348 simple OE retrieval by id. does not look up customer, vendor, units or any other stuff. only oe and orderitems.
1349
1350   my $order = retrieve_simple(id => 2);
1351
1352   $order => {
1353     %_OE_CONTENT,
1354     orderitems => [
1355       %_ORDERITEM_ROW_1,
1356       %_ORDERITEM_ROW_2,
1357       ...
1358     ]
1359   }
1360
1361 =back
1362
1363 =cut