Pflichtenhefte: bei Pflichtenheft → Angebot → Auftrag auch PH mit Auftrag verknüpfen
[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., 51 Franklin Street, Fifth Floor, Boston,
29 # MA 02110-1335, USA.
30 #======================================================================
31 #
32 # Order entry module
33 # Quotation
34 #======================================================================
35
36 package OE;
37
38 use List::Util qw(max first);
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::Project;
46 use SL::DB::ProjectType;
47 use SL::DB::RequirementSpecOrder;
48 use SL::DB::Status;
49 use SL::DB::Tax;
50 use SL::DBUtils;
51 use SL::HTML::Restrict;
52 use SL::IC;
53 use SL::TransNumber;
54 use SL::Util qw(trim);
55 use SL::DB;
56 use SL::YAML;
57 use Text::ParseWords;
58
59 use strict;
60
61 sub transactions {
62   $main::lxdebug->enter_sub();
63
64   my ($self, $myconfig, $form) = @_;
65
66   # connect to database
67   my $dbh = $form->get_standard_dbh;
68
69   my $query;
70   my $ordnumber = 'ordnumber';
71   my $quotation = '0';
72
73   my @values;
74   my $where;
75
76   my ($periodic_invoices_columns, $periodic_invoices_joins);
77
78   my $rate = ($form->{vc} eq 'customer') ? 'buy' : 'sell';
79
80   if ($form->{type} =~ /_quotation$/) {
81     $quotation = '1';
82     $ordnumber = 'quonumber';
83
84   } elsif ($form->{type} eq 'sales_order') {
85     $periodic_invoices_columns = qq| , COALESCE(pcfg.active, 'f') AS periodic_invoices |;
86     $periodic_invoices_joins   = qq| LEFT JOIN periodic_invoices_configs pcfg ON (o.id = pcfg.oe_id) |;
87   }
88
89   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
90
91   my %billed_amount;
92   my %billed_netamount;
93   if ($form->{l_remaining_amount} || $form->{l_remaining_netamount}) {
94     $query = <<'';
95       SELECT from_id, ar.amount, ar.netamount FROM (
96         SELECT from_id, to_id
97         FROM record_links
98         WHERE from_table = 'oe' AND to_table = 'ar'
99         UNION
100         SELECT rl1.from_id, rl2.to_id
101         FROM record_links rl1
102         LEFT JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
103         WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar'
104         UNION
105         SELECT rl1.from_id, rl3.to_id
106         FROM record_links rl1
107         JOIN record_links rl2 ON (rl1.to_table = rl2.from_table AND rl1.to_id = rl2.from_id)
108         JOIN record_links rl3 ON (rl2.to_table = rl3.from_table AND rl2.to_id = rl3.from_id)
109         WHERE rl1.from_table = 'oe' AND rl2.to_table = 'ar' AND rl3.to_table = 'ar'
110       ) rl
111       LEFT JOIN ar ON ar.id = rl.to_id
112
113     for my $ref (@{ selectall_hashref_query($form, $dbh, $query) }) {
114       $billed_amount{   $ref->{from_id}} += $ref->{amount};
115       $billed_netamount{$ref->{from_id}} += $ref->{netamount};
116     }
117   }
118
119   $query =
120     qq|SELECT o.id, o.ordnumber, o.transdate, o.reqdate, | .
121     qq|  o.amount, ct.${vc}number, ct.name, o.netamount, o.${vc}_id, o.globalproject_id, | .
122     qq|  o.closed, o.delivered, o.quonumber, o.cusordnumber, o.shippingpoint, o.shipvia, | .
123     qq|  o.transaction_description, | .
124     qq|  o.marge_total, o.marge_percent, | .
125     qq|  o.exchangerate, | .
126     qq|  o.itime::DATE AS insertdate, | .
127     qq|  o.intnotes, | .
128     qq|  department.description as department, | .
129     qq|  ex.$rate AS daily_exchangerate, | .
130     qq|  pt.description AS payment_terms, | .
131     qq|  pr.projectnumber AS globalprojectnumber, | .
132     qq|  e.name AS employee, s.name AS salesman, | .
133     qq|  ct.${vc}number AS vcnumber, ct.country, ct.ustid, ct.business_id,  | .
134     qq|  tz.description AS taxzone | .
135     $periodic_invoices_columns .
136     qq|  , o.order_probability, o.expected_billing_date, (o.netamount * o.order_probability / 100) AS expected_netamount | .
137     qq|FROM oe o | .
138     qq|JOIN $vc ct ON (o.${vc}_id = ct.id) | .
139     qq|LEFT JOIN contacts cp ON (o.cp_id = cp.cp_id) | .
140     qq|LEFT JOIN employee e ON (o.employee_id = e.id) | .
141     qq|LEFT JOIN employee s ON (o.salesman_id = s.id) | .
142     qq|LEFT JOIN exchangerate ex ON (ex.currency_id = o.currency_id | .
143     qq|  AND ex.transdate = o.transdate) | .
144     qq|LEFT JOIN project pr ON (o.globalproject_id = pr.id) | .
145     qq|LEFT JOIN payment_terms pt ON (pt.id = o.payment_id)| .
146     qq|LEFT JOIN tax_zones tz ON (o.taxzone_id = tz.id) | .
147     qq|LEFT JOIN department   ON (o.department_id = department.id) | .
148     qq|$periodic_invoices_joins | .
149     qq|WHERE (o.quotation = ?) |;
150   push(@values, $quotation);
151
152   if ($form->{department_id}) {
153     $query .= qq| AND o.department_id = ?|;
154     push(@values, $form->{department_id});
155   }
156
157   if ($form->{"project_id"}) {
158     $query .=
159       qq|AND ((globalproject_id = ?) OR EXISTS | .
160       qq|  (SELECT * FROM orderitems oi | .
161       qq|   WHERE oi.project_id = ? AND oi.trans_id = o.id))|;
162     push(@values, conv_i($form->{"project_id"}), conv_i($form->{"project_id"}));
163   }
164
165   if ($form->{"projectnumber"}) {
166     $query .= <<SQL;
167       AND ((pr.projectnumber ILIKE ?) OR EXISTS (
168         SELECT * FROM orderitems oi
169         LEFT JOIN project proi ON proi.id = oi.project_id
170         WHERE proi.projectnumber ILIKE ? AND oi.trans_id = o.id
171       ))
172 SQL
173     push @values, like($form->{"projectnumber"}), like($form->{"projectnumber"});
174   }
175
176   if ($form->{"business_id"}) {
177     $query .= " AND ct.business_id = ?";
178     push(@values, $form->{"business_id"});
179   }
180
181   if ($form->{"${vc}_id"}) {
182     $query .= " AND o.${vc}_id = ?";
183     push(@values, $form->{"${vc}_id"});
184
185   } elsif ($form->{$vc}) {
186     $query .= " AND ct.name ILIKE ?";
187     push(@values, like($form->{$vc}));
188   }
189
190   if ($form->{"cp_name"}) {
191     $query .= " AND (cp.cp_name ILIKE ? OR cp.cp_givenname ILIKE ?)";
192     push(@values, (like($form->{"cp_name"}))x2);
193   }
194
195   if ( !(($vc eq 'customer' && $main::auth->assert('sales_all_edit', 1)) || ($vc eq 'vendor' && $main::auth->assert('purchase_all_edit', 1))) ) {
196     $query .= " AND o.employee_id = (select id from employee where login= ?)";
197     push @values, $::myconfig{login};
198   }
199   if ($form->{employee_id}) {
200     $query .= " AND o.employee_id = ?";
201     push @values, conv_i($form->{employee_id});
202   }
203
204   if ($form->{salesman_id}) {
205     $query .= " AND o.salesman_id = ?";
206     push @values, conv_i($form->{salesman_id});
207   }
208
209   if (!$form->{open} && !$form->{closed}) {
210     $query .= " AND o.id = 0";
211   } elsif (!($form->{open} && $form->{closed})) {
212     $query .= ($form->{open}) ? " AND o.closed = '0'" : " AND o.closed = '1'";
213   }
214
215   if (($form->{"notdelivered"} || $form->{"delivered"}) &&
216       ($form->{"notdelivered"} ne $form->{"delivered"})) {
217     $query .= $form->{"delivered"} ?
218       " AND o.delivered " : " AND NOT o.delivered";
219   }
220
221   if ($form->{$ordnumber}) {
222     $query .= qq| AND o.$ordnumber ILIKE ?|;
223     push(@values, like($form->{$ordnumber}));
224   }
225
226   if ($form->{cusordnumber}) {
227     $query .= qq| AND o.cusordnumber ILIKE ?|;
228     push(@values, like($form->{cusordnumber}));
229   }
230
231   if($form->{transdatefrom}) {
232     $query .= qq| AND o.transdate >= ?|;
233     push(@values, conv_date($form->{transdatefrom}));
234   }
235
236   if($form->{transdateto}) {
237     $query .= qq| AND o.transdate <= ?|;
238     push(@values, conv_date($form->{transdateto}));
239   }
240
241   if($form->{reqdatefrom}) {
242     $query .= qq| AND o.reqdate >= ?|;
243     push(@values, conv_date($form->{reqdatefrom}));
244   }
245
246   if($form->{reqdateto}) {
247     $query .= qq| AND o.reqdate <= ?|;
248     push(@values, conv_date($form->{reqdateto}));
249   }
250
251   if($form->{insertdatefrom}) {
252     $query .= qq| AND o.itime::DATE >= ?|;
253     push(@values, conv_date($form->{insertdatefrom}));
254   }
255
256   if($form->{insertdateto}) {
257     $query .= qq| AND o.itime::DATE <= ?|;
258     push(@values, conv_date($form->{insertdateto}));
259   }
260
261   if ($form->{shippingpoint}) {
262     $query .= qq| AND o.shippingpoint ILIKE ?|;
263     push(@values, like($form->{shippingpoint}));
264   }
265
266   if ($form->{taxzone_id} ne '') { # taxzone_id could be 0
267     $query .= qq| AND tz.id = ?|;
268     push(@values, $form->{taxzone_id});
269   }
270
271   if ($form->{transaction_description}) {
272     $query .= qq| AND o.transaction_description ILIKE ?|;
273     push(@values, like($form->{transaction_description}));
274   }
275
276   if ($form->{periodic_invoices_active} ne $form->{periodic_invoices_inactive}) {
277     my $not  = $form->{periodic_invoices_inactive} ? 'NOT' : '';
278     $query  .= qq| AND ${not} COALESCE(pcfg.active, 'f')|;
279   }
280
281   if ($form->{reqdate_unset_or_old}) {
282     $query .= qq| AND ((o.reqdate IS NULL) OR (o.reqdate < date_trunc('month', current_date)))|;
283   }
284
285   if (($form->{order_probability_value} || '') ne '') {
286     my $op  = $form->{order_probability_value} eq 'le' ? '<=' : '>=';
287     $query .= qq| AND (o.order_probability ${op} ?)|;
288     push @values, trim($form->{order_probability_value});
289   }
290
291   if ($form->{expected_billing_date_from}) {
292     $query .= qq| AND (o.expected_billing_date >= ?)|;
293     push @values, conv_date($form->{expected_billing_date_from});
294   }
295
296   if ($form->{expected_billing_date_to}) {
297     $query .= qq| AND (o.expected_billing_date <= ?)|;
298     push @values, conv_date($form->{expected_billing_date_to});
299   }
300
301   if ($form->{intnotes}) {
302     $query .= qq| AND o.intnotes ILIKE ?|;
303     push(@values, like($form->{intnotes}));
304   }
305
306   if ($form->{parts_partnumber}) {
307     $query .= <<SQL;
308       AND EXISTS (
309         SELECT orderitems.trans_id
310         FROM orderitems
311         LEFT JOIN parts ON (orderitems.parts_id = parts.id)
312         WHERE (orderitems.trans_id = o.id)
313           AND (parts.partnumber ILIKE ?)
314         LIMIT 1
315       )
316 SQL
317     push @values, like($form->{parts_partnumber});
318   }
319
320   if ($form->{parts_description}) {
321     $query .= <<SQL;
322       AND EXISTS (
323         SELECT orderitems.trans_id
324         FROM orderitems
325         WHERE (orderitems.trans_id = o.id)
326           AND (orderitems.description ILIKE ?)
327         LIMIT 1
328       )
329 SQL
330     push @values, like($form->{parts_description});
331   }
332
333   if ($form->{all}) {
334     my @tokens = parse_line('\s+', 0, $form->{all});
335     # ordnumber quonumber customer.name vendor.name transaction_description
336     $query .= qq| AND (
337       o.ordnumber ILIKE ? OR
338       o.quonumber ILIKE ? OR
339       ct.name     ILIKE ? OR
340       o.transaction_description ILIKE ?
341     )| for @tokens;
342     push @values, (like($_))x4 for @tokens;
343   }
344
345   my ($cvar_where, @cvar_values) = CVar->build_filter_query('module'         => 'CT',
346                                                             'trans_id_field' => 'ct.id',
347                                                             'filter'         => $form,
348                                                            );
349   if ($cvar_where) {
350     $query .= qq| AND ($cvar_where)|;
351     push @values, @cvar_values;
352   }
353
354   my $sortdir   = !defined $form->{sortdir} ? 'ASC' : $form->{sortdir} ? 'ASC' : 'DESC';
355   my $sortorder = join(', ', map { "${_} ${sortdir} " } ("o.id", $form->sort_columns("transdate", $ordnumber, "name"), "o.itime"));
356   my %allowed_sort_columns = (
357     "transdate"               => "o.transdate",
358     "reqdate"                 => "o.reqdate",
359     "id"                      => "o.id",
360     "ordnumber"               => "o.ordnumber",
361     "cusordnumber"            => "o.cusordnumber",
362     "quonumber"               => "o.quonumber",
363     "name"                    => "ct.name",
364     "employee"                => "e.name",
365     "salesman"                => "s.name",
366     "shipvia"                 => "o.shipvia",
367     "transaction_description" => "o.transaction_description",
368     "shippingpoint"           => "o.shippingpoint",
369     "insertdate"              => "o.itime",
370     "taxzone"                 => "tz.description",
371     "payment_terms"           => "pt.description",
372     "department"              => "department.description",
373     "intnotes"                => "o.intnotes",
374   );
375   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
376     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}"  . ", o.itime ${sortdir}";
377   }
378   $query .= qq| ORDER by | . $sortorder;
379
380   my $sth = $dbh->prepare($query);
381   $sth->execute(@values) ||
382     $form->dberror($query . " (" . join(", ", @values) . ")");
383
384   my %id = ();
385   $form->{OE} = [];
386   while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
387     $ref->{billed_amount}    = $billed_amount{$ref->{id}};
388     $ref->{billed_netamount} = $billed_netamount{$ref->{id}};
389     if ($ref->{billed_amount} < 0) { # case: credit note(s) higher than invoices
390       $ref->{remaining_amount} = $ref->{amount} + $ref->{billed_amount};
391       $ref->{remaining_netamount} = $ref->{netamount} + $ref->{billed_netamount};
392     } else {
393       $ref->{remaining_amount} = $ref->{amount} - $ref->{billed_amount};
394       $ref->{remaining_netamount} = $ref->{netamount} - $ref->{billed_netamount};
395     }
396     $ref->{exchangerate} ||= $ref->{daily_exchangerate};
397     $ref->{exchangerate} ||= 1;
398     push @{ $form->{OE} }, $ref if $ref->{id} != $id{ $ref->{id} };
399     $id{ $ref->{id} } = $ref->{id};
400   }
401
402   $sth->finish;
403
404   $main::lxdebug->leave_sub();
405 }
406
407 sub transactions_for_todo_list {
408   $main::lxdebug->enter_sub();
409
410   my $self     = shift;
411   my %params   = @_;
412
413   my $myconfig = \%main::myconfig;
414   my $form     = $main::form;
415
416   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
417
418   my $query    = qq|SELECT id FROM employee WHERE login = ?|;
419   my ($e_id)   = selectrow_query($form, $dbh, $query, $::myconfig{login});
420
421   $query       =
422     qq|SELECT oe.id, oe.transdate, oe.reqdate, oe.quonumber, oe.transaction_description, oe.amount,
423          CASE WHEN (COALESCE(oe.customer_id, 0) = 0) THEN 'vendor' ELSE 'customer' END AS vc,
424          c.name AS customer,
425          v.name AS vendor,
426          e.name AS employee
427        FROM oe
428        LEFT JOIN customer c ON (oe.customer_id = c.id)
429        LEFT JOIN vendor v   ON (oe.vendor_id   = v.id)
430        LEFT JOIN employee e ON (oe.employee_id = e.id)
431        WHERE (COALESCE(quotation, FALSE) = TRUE)
432          AND (COALESCE(closed,    FALSE) = FALSE)
433          AND ((oe.employee_id = ?) OR (oe.salesman_id = ?))
434          AND NOT (oe.reqdate ISNULL)
435          AND (oe.reqdate < current_date)
436        ORDER BY transdate|;
437
438   my $quotations = selectall_hashref_query($form, $dbh, $query, $e_id, $e_id);
439
440   $main::lxdebug->leave_sub();
441
442   return $quotations;
443 }
444
445 sub save {
446   my ($self, $myconfig, $form) = @_;
447   $main::lxdebug->enter_sub();
448
449   my $rc = SL::DB->client->with_transaction(\&_save, $self, $myconfig, $form);
450
451   $::lxdebug->leave_sub;
452
453   return $rc;
454 }
455
456 sub _save {
457   $main::lxdebug->enter_sub();
458
459   my ($self, $myconfig, $form) = @_;
460
461   my $dbh = SL::DB->client->dbh;
462   my $restricter = SL::HTML::Restrict->create;
463
464   my ($query, @values, $sth, $null);
465   my $exchangerate = 0;
466
467   my $all_units = AM->retrieve_units($myconfig, $form);
468   $form->{all_units} = $all_units;
469
470   my $ic_cvar_configs = CVar->get_configs(module => 'IC',
471                                           dbh    => $dbh);
472
473   $form->{employee_id} = (split /--/, $form->{employee})[1] if !$form->{employee_id};
474   unless ($form->{employee_id}) {
475     $form->get_employee($dbh);
476   }
477
478   my $ml = ($form->{type} eq 'sales_order') ? 1 : -1;
479
480   my $number_field         = $form->{type} =~ m{order} ? 'ordnumber' : 'quonumber';
481   my $trans_number         = SL::TransNumber->new(type => $form->{type}, dbh => $dbh, number => $form->{$number_field}, id => $form->{id});
482   $form->{$number_field} ||= $trans_number->create_unique; # set $form->{ordnumber} or $form->{quonumber}
483   my $is_new               = !$form->{id};
484
485   if ($form->{id}) {
486     $query = qq|DELETE FROM custom_variables
487                 WHERE (config_id IN (SELECT id        FROM custom_variable_configs WHERE (module = 'ShipTo')))
488                   AND (trans_id  IN (SELECT shipto_id FROM shipto                  WHERE (module = 'OE') AND (trans_id = ?)))|;
489     do_query($form, $dbh, $query, $form->{id});
490
491     $query = qq|DELETE FROM shipto | .
492              qq|WHERE trans_id = ? AND module = 'OE'|;
493     do_query($form, $dbh, $query, $form->{id});
494
495   } else {
496
497     $query = qq|SELECT nextval('id')|;
498     ($form->{id}) = selectrow_query($form, $dbh, $query);
499
500     $query = qq|INSERT INTO oe (id, ordnumber, employee_id, currency_id, taxzone_id) VALUES (?, '', ?, (SELECT currency_id FROM defaults), ?)|;
501     do_query($form, $dbh, $query, $form->{id}, $form->{employee_id}, $form->{taxzone_id});
502   }
503
504   my $amount    = 0;
505   my $linetotal = 0;
506   my $discount  = 0;
507   my $project_id;
508   my $reqdate;
509   my $taxrate;
510   my $taxbase;
511   my $taxdiff;
512   my $taxamount = 0;
513   my $fxsellprice;
514   my %taxbase;
515   my @taxaccounts;
516   my %taxaccounts;
517   my $netamount = 0;
518   my @processed_orderitems;
519
520   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
521   my %price_factors = map { $_->{id} => $_->{factor} } @{ $form->{ALL_PRICE_FACTORS} };
522   my $price_factor;
523
524   for my $i (1 .. $form->{rowcount}) {
525
526     map({ $form->{"${_}_$i"} = $form->parse_amount($myconfig, $form->{"${_}_$i"}) } qw(qty ship));
527
528     if ($form->{"id_$i"}) {
529
530       # get item baseunit
531       $query = qq|SELECT unit FROM parts WHERE id = ?|;
532       my ($item_unit) = selectrow_query($form, $dbh, $query, $form->{"id_$i"});
533
534       my $basefactor = 1;
535       if (defined($all_units->{$item_unit}->{factor}) &&
536           (($all_units->{$item_unit}->{factor} * 1) != 0)) {
537         $basefactor = $all_units->{$form->{"unit_$i"}}->{factor} / $all_units->{$item_unit}->{factor};
538       }
539       my $baseqty = $form->{"qty_$i"} * $basefactor;
540
541       $form->{"marge_percent_$i"} = $form->parse_amount($myconfig, $form->{"marge_percent_$i"}) * 1;
542       $form->{"marge_absolut_$i"} = $form->parse_amount($myconfig, $form->{"marge_absolut_$i"}) * 1;
543
544       $form->{"lastcost_$i"} = $form->parse_amount($myconfig, $form->{"lastcost_$i"});
545
546       # keep entered selling price
547       my $fxsellprice =
548         $form->parse_amount($myconfig, $form->{"sellprice_$i"});
549
550       my ($dec) = ($fxsellprice =~ /\.(\d+)/);
551       $dec = length $dec;
552       my $decimalplaces = ($dec > 2) ? $dec : 2;
553
554       # undo discount formatting
555       $form->{"discount_$i"} = $form->parse_amount($myconfig, $form->{"discount_$i"}) / 100;
556
557       # deduct discount
558       $form->{"sellprice_$i"} = $fxsellprice * (1 - $form->{"discount_$i"});
559
560       # round linetotal at least to 2 decimal places
561       $price_factor = $price_factors{ $form->{"price_factor_id_$i"} } || 1;
562       $linetotal    = $form->round_amount($form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor, 2);
563
564       $form->{"inventory_accno_$i"} *= 1;
565       $form->{"expense_accno_$i"}   *= 1;
566
567       @taxaccounts = split(/ /, $form->{"taxaccounts_$i"});
568       $taxrate     = 0;
569       $taxdiff     = 0;
570
571       map { $taxrate += $form->{"${_}_rate"} } @taxaccounts;
572
573       if ($form->{taxincluded}) {
574         $taxamount = $linetotal * $taxrate / (1 + $taxrate);
575         $taxbase   = $linetotal - $taxamount;
576
577         # we are not keeping a natural price, do not round
578         $form->{"sellprice_$i"} =
579           $form->{"sellprice_$i"} * (1 / (1 + $taxrate));
580       } else {
581         $taxamount = $linetotal * $taxrate;
582         $taxbase   = $linetotal;
583       }
584
585       if ($form->round_amount($taxrate, 7) == 0) {
586         if ($form->{taxincluded}) {
587           foreach my $item (@taxaccounts) {
588             $taxamount = $form->round_amount($linetotal * $form->{"${item}_rate"} / (1 + abs($form->{"${item}_rate"})), 2);
589             $taxaccounts{$item} += $taxamount;
590             $taxdiff            += $taxamount;
591             $taxbase{$item}     += $taxbase;
592           }
593           $taxaccounts{ $taxaccounts[0] } += $taxdiff;
594         } else {
595           foreach my $item (@taxaccounts) {
596             $taxaccounts{$item} += $linetotal * $form->{"${item}_rate"};
597             $taxbase{$item}     += $taxbase;
598           }
599         }
600       } else {
601         foreach my $item (@taxaccounts) {
602           $taxaccounts{$item} += $taxamount * $form->{"${item}_rate"} / $taxrate;
603           $taxbase{$item} += $taxbase;
604         }
605       }
606
607       $netamount += $form->{"sellprice_$i"} * $form->{"qty_$i"} / $price_factor;
608
609       $reqdate = ($form->{"reqdate_$i"}) ? $form->{"reqdate_$i"} : undef;
610
611       # Get pricegroup_id and save it. Unfortunately the interface
612       # also uses ID "0" for signalling that none is selected, but "0"
613       # must not be stored in the database. Therefore we cannot simply
614       # use conv_i().
615       ($null, my $pricegroup_id) = split(/--/, $form->{"sellprice_pg_$i"});
616       $pricegroup_id *= 1;
617       $pricegroup_id  = undef if !$pricegroup_id;
618
619       # force new project, if not set yet
620       if ($::instance_conf->get_order_always_project && !$form->{"globalproject_id"} && ($form->{type} eq 'sales_order')) {
621         require SL::DB::Customer;
622         my $customer = SL::DB::Manager::Customer->find_by(id => $form->{customer_id});
623         die "Can't find customer" unless $customer;
624         die $main::locale->text("Error while creating project with project number of new order number, project number #1 already exists!", $form->{ordnumber})
625           if SL::DB::Manager::Project->find_by(projectnumber => $form->{ordnumber});
626
627         my $new_project = SL::DB::Project->new(
628           projectnumber     => $form->{ordnumber},
629           description       => $customer->name,
630           customer_id       => $customer->id,
631           active            => 1,
632           project_type_id   => $::instance_conf->get_project_type_id,
633           project_status_id => $::instance_conf->get_project_status_id,
634         );
635         $new_project->save;
636         $form->{"globalproject_id"} = $new_project->id;
637       }
638
639       CVar->get_non_editable_ic_cvars(form               => $form,
640                                       dbh                => $dbh,
641                                       row                => $i,
642                                       sub_module         => 'orderitems',
643                                       may_converted_from => ['orderitems', 'invoice']);
644
645       my $position = $i;
646
647       # save detail record in orderitems table
648       if (! $form->{"orderitems_id_$i"}) {
649         $query = qq|SELECT nextval('orderitemsid')|;
650         ($form->{"orderitems_id_$i"}) = selectrow_query($form, $dbh, $query);
651
652         $query = qq|INSERT INTO orderitems (id, position) VALUES (?, ?)|;
653         do_query($form, $dbh, $query, $form->{"orderitems_id_$i"}, conv_i($position));
654       }
655
656       my $orderitems_id = $form->{"orderitems_id_$i"};
657       push @processed_orderitems, $orderitems_id;
658
659        $query = <<SQL;
660          UPDATE orderitems SET
661           trans_id = ?, position = ?, parts_id = ?, description = ?, longdescription = ?, qty = ?, base_qty = ?,
662           sellprice = ?, discount = ?, unit = ?, reqdate = ?, project_id = ?, serialnumber = ?, ship = ?,
663           pricegroup_id = ?, subtotal = ?,
664           marge_percent = ?, marge_total = ?, lastcost = ?, price_factor_id = ?,
665           active_price_source = ?, active_discount_source = ?,
666           price_factor = (SELECT factor FROM price_factors WHERE id = ?), marge_price_factor = ?
667         WHERE id = ?
668 SQL
669       @values = (
670            conv_i($form->{id}), conv_i($position), conv_i($form->{"id_$i"}),
671            $form->{"description_$i"}, $restricter->process($form->{"longdescription_$i"}),
672            $form->{"qty_$i"}, $baseqty,
673            $fxsellprice, $form->{"discount_$i"},
674            $form->{"unit_$i"}, conv_date($reqdate), conv_i($form->{"project_id_$i"}),
675            $form->{"serialnumber_$i"}, $form->{"ship_$i"},
676            $pricegroup_id, $form->{"subtotal_$i"} ? 't' : 'f',
677            $form->{"marge_percent_$i"}, $form->{"marge_absolut_$i"},
678            $form->{"lastcost_$i"}, conv_i($form->{"price_factor_id_$i"}),
679            $form->{"active_price_source_$i"}, $form->{"active_discount_source_$i"},
680            conv_i($form->{"price_factor_id_$i"}), conv_i($form->{"marge_price_factor_$i"}),
681            conv_i($orderitems_id),
682       );
683
684       do_query($form, $dbh, $query, @values);
685
686       $form->{"sellprice_$i"} = $fxsellprice;
687       $form->{"discount_$i"} *= 100;
688
689       CVar->save_custom_variables(module       => 'IC',
690                                   sub_module   => 'orderitems',
691                                   trans_id     => $orderitems_id,
692                                   configs      => $ic_cvar_configs,
693                                   variables    => $form,
694                                   name_prefix  => 'ic_',
695                                   name_postfix => "_$i",
696                                   dbh          => $dbh);
697
698       # link previous items with orderitems
699       # assume we have a new workflow if we link from invoice or order to quotation
700       # unluckily orderitems are used for quotation and orders - therefore one more
701       # check to be sure NOT to link from order to quotation
702       foreach (qw(orderitems)) {
703         if (!$form->{saveasnew} && !$form->{useasnew} && $form->{"converted_from_${_}_id_$i"}
704               && $form->{type} !~ 'quotation') {
705           RecordLinks->create_links('dbh'        => $dbh,
706                                     'mode'       => 'ids',
707                                     'from_table' => $_,
708                                     'from_ids'   => $form->{"converted_from_${_}_id_$i"},
709                                     'to_table'   => 'orderitems',
710                                     'to_id'      => $orderitems_id,
711           );
712         }
713         delete $form->{"converted_from_${_}_id_$i"};
714       }
715     }
716   }
717
718   # search for orphaned ids
719   $query  = sprintf 'SELECT id FROM orderitems WHERE trans_id = ? AND NOT id IN (%s)', join ', ', ("?") x scalar @processed_orderitems;
720   @values = (conv_i($form->{id}), map { conv_i($_) } @processed_orderitems);
721   my @orphaned_ids = map { $_->{id} } selectall_hashref_query($form, $dbh, $query, @values);
722
723   if (scalar @orphaned_ids) {
724     # clean up orderitems
725     $query  = sprintf 'DELETE FROM orderitems WHERE id IN (%s)', join ', ', ("?") x scalar @orphaned_ids;
726     do_query($form, $dbh, $query, @orphaned_ids);
727   }
728
729   $reqdate = ($form->{reqdate}) ? $form->{reqdate} : undef;
730
731   # add up the tax
732   my $tax = 0;
733   map { $tax += $form->round_amount($taxaccounts{$_}, 2) } keys %taxaccounts;
734
735   $amount = $form->round_amount($netamount + $tax, 2, 1);
736   $netamount = $form->round_amount($netamount, 2);
737
738   if ($form->{currency} eq $form->{defaultcurrency}) {
739     $form->{exchangerate} = 1;
740   } else {
741     $exchangerate = $form->check_exchangerate($myconfig, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? 'buy' : 'sell');
742   }
743
744   # from inputfield (exchangerate) or hidden (forex)
745   my $exchangerate_from_form = $form->{forex} || $form->parse_amount($myconfig, $form->{exchangerate});
746
747   $form->{exchangerate} = $exchangerate || $exchangerate_from_form;
748
749   my $quotation = $form->{type} =~ /_order$/ ? 'f' : 't';
750
751   # save OE record
752   $query =
753     qq|UPDATE oe SET
754          ordnumber = ?, quonumber = ?, cusordnumber = ?, transdate = ?, vendor_id = ?,
755          customer_id = ?, amount = ?, netamount = ?, reqdate = ?, tax_point = ?, taxincluded = ?,
756          shippingpoint = ?, shipvia = ?, notes = ?, intnotes = ?, currency_id = (SELECT id FROM currencies WHERE name=?), closed = ?,
757          delivered = ?, proforma = ?, quotation = ?, department_id = ?, language_id = ?,
758          taxzone_id = ?, shipto_id = ?, payment_id = ?, delivery_vendor_id = ?, delivery_customer_id = ?,delivery_term_id = ?,
759          globalproject_id = ?, employee_id = ?, salesman_id = ?, cp_id = ?, transaction_description = ?, marge_total = ?, marge_percent = ?
760          , order_probability = ?, expected_billing_date = ?
761        WHERE id = ?|;
762
763   @values = ($form->{ordnumber} || '', $form->{quonumber},
764              $form->{cusordnumber}, conv_date($form->{transdate}),
765              conv_i($form->{vendor_id}), conv_i($form->{customer_id}),
766              $amount, $netamount, conv_date($reqdate), conv_date($form->{tax_point}),
767              $form->{taxincluded} ? 't' : 'f', $form->{shippingpoint},
768              $form->{shipvia}, $restricter->process($form->{notes}), $form->{intnotes},
769              $form->{currency}, $form->{closed} ? 't' : 'f',
770              $form->{delivered} ? "t" : "f", $form->{proforma} ? 't' : 'f',
771              $quotation, conv_i($form->{department_id}),
772              conv_i($form->{language_id}), conv_i($form->{taxzone_id}),
773              conv_i($form->{shipto_id}), conv_i($form->{payment_id}),
774              conv_i($form->{delivery_vendor_id}),
775              conv_i($form->{delivery_customer_id}),
776              conv_i($form->{delivery_term_id}),
777              conv_i($form->{globalproject_id}), conv_i($form->{employee_id}),
778              conv_i($form->{salesman_id}), conv_i($form->{cp_id}),
779              $form->{transaction_description},
780              $form->{marge_total} * 1, $form->{marge_percent} * 1,
781              $form->{order_probability} * 1, conv_date($form->{expected_billing_date}),
782              conv_i($form->{id}));
783   do_query($form, $dbh, $query, @values);
784
785   $form->new_lastmtime('oe');
786
787   $form->{ordtotal} = $amount;
788
789   $form->{name} = $form->{ $form->{vc} };
790   $form->{name} =~ s/--\Q$form->{"$form->{vc}_id"}\E//;
791
792   # add shipto
793   if (!$form->{shipto_id}) {
794     $form->add_shipto($dbh, $form->{id}, "OE");
795   }
796
797   # save printed, emailed, queued
798   $form->save_status($dbh);
799
800   # Link this record to the records it was created from.
801   $form->{convert_from_oe_ids} =~ s/^\s+//;
802   $form->{convert_from_oe_ids} =~ s/\s+$//;
803   my @convert_from_oe_ids      =  split m/\s+/, $form->{convert_from_oe_ids};
804   delete $form->{convert_from_oe_ids};
805   if (!$form->{useasnew} && scalar @convert_from_oe_ids) {
806     RecordLinks->create_links('dbh'        => $dbh,
807                               'mode'       => 'ids',
808                               'from_table' => 'oe',
809                               'from_ids'   => \@convert_from_oe_ids,
810                               'to_table'   => 'oe',
811                               'to_id'      => $form->{id},
812       );
813     $self->_close_quotations_rfqs('dbh'     => $dbh,
814                                   'from_id' => \@convert_from_oe_ids,
815                                   'to_id'   => $form->{id});
816   }
817
818   if (($form->{currency} ne $form->{defaultcurrency}) && !$exchangerate) {
819     if ($form->{vc} eq 'customer') {
820       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, $form->{exchangerate}, 0);
821     }
822     if ($form->{vc} eq 'vendor') {
823       $form->update_exchangerate($dbh, $form->{currency}, $form->{transdate}, 0, $form->{exchangerate});
824     }
825   }
826
827   $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ?
828                                        "quonumber" : "ordnumber"};
829
830   Common::webdav_folder($form);
831
832   $self->save_periodic_invoices_config(dbh         => $dbh,
833                                        oe_id       => $form->{id},
834                                        config_yaml => $form->{periodic_invoices_config})
835     if ($form->{type} eq 'sales_order');
836
837   $self->_link_created_sales_order_to_requirement_specs_for_sales_quotations(
838     type               => $form->{type},
839     converted_from_ids => \@convert_from_oe_ids,
840     sales_order_id     => $form->{id},
841     is_new             => $is_new,
842   );
843
844   $main::lxdebug->leave_sub();
845
846   return 1;
847 }
848
849 sub _link_created_sales_order_to_requirement_specs_for_sales_quotations {
850   my ($self, %params) = @_;
851
852   # If this is a sales order created from a sales quotation and if
853   # that sales quotation was created from a requirement spec document
854   # then link the newly created sales order to the requirement spec
855   # document, too.
856
857   return if !$params{is_new};
858   return if  $params{type} ne 'sales_order';
859   return if !@{ $params{converted_from_ids} };
860
861   my $oe_objects       = SL::DB::Manager::Order->get_all(where => [ id => $params{converted_from_ids} ]);
862   my @sales_quotations = grep { $_->is_type('sales_quotation') } @{ $oe_objects };
863
864   return if !@sales_quotations;
865
866   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => [ map { $_->id } @sales_quotations ] ]);
867
868   return if !@{ $rs_orders };
869
870   $rs_orders->[0]->db->with_transaction(sub {
871     foreach my $rs_order (@{ $rs_orders }) {
872       SL::DB::RequirementSpecOrder->new(
873         order_id            => $params{sales_order_id},
874         requirement_spec_id => $rs_order->requirement_spec_id,
875         version_id          => $rs_order->version_id,
876       )->save;
877     }
878
879     1;
880   });
881 }
882
883 sub save_periodic_invoices_config {
884   my ($self, %params) = @_;
885
886   return if !$params{oe_id};
887
888   my $config = $params{config_yaml} ? SL::YAML::Load($params{config_yaml}) : undef;
889   return if 'HASH' ne ref $config;
890
891   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
892           || SL::DB::PeriodicInvoicesConfig->new(oe_id => $params{oe_id});
893   $obj->update_attributes(%{ $config });
894 }
895
896 sub load_periodic_invoice_config {
897   my $self = shift;
898   my $form = shift;
899
900   delete $form->{periodic_invoices_config};
901
902   if ($form->{id}) {
903     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
904
905     if ($config_obj) {
906       my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity order_value_periodicity start_date_as_date end_date_as_date first_billing_date_as_date extend_automatically_by ar_chart_id
907                                                       print printer_id copies direct_debit send_email email_recipient_contact_id email_recipient_address email_sender email_subject email_body) };
908       $form->{periodic_invoices_config} = SL::YAML::Dump($config);
909     }
910   }
911 }
912
913 sub _close_quotations_rfqs {
914   $main::lxdebug->enter_sub();
915
916   my $self     = shift;
917   my %params   = @_;
918
919   Common::check_params(\%params, qw(from_id to_id));
920
921   my $myconfig = \%main::myconfig;
922   my $form     = $main::form;
923
924   my $dbh      = $params{dbh} || SL::DB->client->dbh;
925
926   SL::DB->client->with_transaction(sub {
927
928     my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
929     my $sth      = prepare_query($form, $dbh, $query);
930
931     do_statement($form, $sth, $query, conv_i($params{to_id}));
932
933     my ($quotation) = $sth->fetchrow_array();
934
935     if ($quotation) {
936       return 1;
937     }
938
939     my @close_ids;
940
941     foreach my $from_id (@{ $params{from_id} }) {
942       $from_id = conv_i($from_id);
943       do_statement($form, $sth, $query, $from_id);
944       ($quotation) = $sth->fetchrow_array();
945       push @close_ids, $from_id if ($quotation);
946     }
947
948     $sth->finish();
949
950     if (scalar @close_ids) {
951       $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
952       do_query($form, $dbh, $query, @close_ids);
953     }
954     1;
955   }) or do { die SL::DB->client->error };
956
957   $main::lxdebug->leave_sub();
958 }
959
960 sub delete {
961   $main::lxdebug->enter_sub();
962
963   my ($self, $myconfig, $form) = @_;
964
965   my $rc = SL::DB::Order->new->db->with_transaction(sub {
966     my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $form->{id} ]) };
967
968     SL::DB::Order->new(id => $form->{id})->delete;
969
970     my $spool = $::lx_office_conf{paths}->{spool};
971     unlink map { "$spool/$_" } @spoolfiles if $spool;
972
973     1;
974   }) or do { die SL::DB->client->error };
975
976   $main::lxdebug->leave_sub();
977
978   return $rc;
979 }
980
981 sub retrieve {
982   my ($self, $myconfig, $form) = @_;
983   $main::lxdebug->enter_sub();
984
985   my $rc = SL::DB->client->with_transaction(\&_retrieve, $self, $myconfig, $form);
986
987   $::lxdebug->leave_sub;
988   return $rc;
989 }
990
991 sub _retrieve {
992   my ($self, $myconfig, $form) = @_;
993
994   # connect to database
995   my $dbh = SL::DB->client->dbh;
996
997   my ($query, $query_add, @values, @ids, $sth);
998
999   # translate the ids (given by id_# and trans_id_#) into one array of ids, so we can join them later
1000   map {
1001     push @ids, $form->{"trans_id_$_"}
1002       if ($form->{"multi_id_$_"} and $form->{"trans_id_$_"})
1003   } (1 .. $form->{"rowcount"});
1004
1005   if ($form->{rowcount} && scalar @ids) {
1006     $form->{convert_from_oe_ids} = join ' ', @ids;
1007   }
1008
1009   # if called in multi id mode, and still only got one id, switch back to single id
1010   if ($form->{"rowcount"} and $#ids == 0) {
1011     $form->{"id"} = $ids[0];
1012     undef @ids;
1013     delete $form->{convert_from_oe_ids};
1014   }
1015
1016   # and remember for the rest of the function
1017   my $is_collective_order = scalar @ids;
1018
1019   # If collective order was created from exactly 1 order, we assume the same
1020   # behaviour as a "save as new" from within an order is actually desired, i.e.
1021   # the original order isn't part of a workflow where we want to remember
1022   # record_links, but simply a quick way of generating a new order from an old
1023   # one without having to enter everything again.
1024   # Setting useasnew will prevent the creation of record_links for the items
1025   # when saving the new order.
1026   # This form variable is probably not necessary, could just set saveasnew instead
1027   $form->{useasnew} = 1 if $is_collective_order == 1;
1028
1029   if (!$form->{id}) {
1030     my $extra_days = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
1031                      $form->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
1032     if (   ($form->{type} eq 'sales_order'     &&  !$::instance_conf->get_deliverydate_on)
1033         || ($form->{type} eq 'sales_quotation' &&  !$::instance_conf->get_reqdate_on)) {
1034       $form->{reqdate}   = '';
1035     } else {
1036       $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
1037     }
1038     $form->{transdate} = DateTime->today_local->to_kivitendo;
1039   }
1040
1041   # get default accounts
1042   $query = qq|SELECT (SELECT c.accno FROM chart c WHERE d.inventory_accno_id = c.id) AS inventory_accno,
1043                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
1044                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
1045                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
1046                      (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno,
1047                      (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id   = c.id) AS rndgain_accno,
1048                      (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id   = c.id) AS rndloss_accno
1049               $query_add
1050               FROM defaults d|;
1051   my $ref = selectfirst_hashref_query($form, $dbh, $query);
1052   map { $form->{$_} = $ref->{$_} } keys %$ref;
1053
1054   $form->{currency} = $form->get_default_currency($myconfig);
1055
1056   # set reqdate if this is an invoice->order conversion. If someone knows a better check to ensure
1057   # we come from invoices, feel free.
1058   $form->{reqdate} = $form->{deliverydate}
1059     if (    $form->{deliverydate}
1060         and $form->{callback} =~ /action=ar_transactions/);
1061
1062   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
1063
1064   if ($form->{id} or @ids) {
1065
1066     # retrieve order for single id
1067     # NOTE: this query is intended to fetch all information only ONCE.
1068     # so if any of these infos is important (or even different) for any item,
1069     # it will be killed out and then has to be fetched from the item scope query further down
1070     $query =
1071       qq|SELECT o.cp_id, o.ordnumber, o.transdate, o.reqdate,
1072            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
1073            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
1074            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
1075            o.closed, o.reqdate, o.tax_point, o.quonumber, o.department_id, o.cusordnumber,
1076            o.mtime, o.itime,
1077            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
1078            o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
1079            o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id,
1080            o.itime::DATE AS insertdate, o.order_probability, o.expected_billing_date
1081          FROM oe o
1082          JOIN ${vc} cv ON (o.${vc}_id = cv.id)
1083          LEFT JOIN employee e ON (o.employee_id = e.id)
1084          LEFT JOIN department d ON (o.department_id = d.id) | .
1085         ($form->{id}
1086          ? "WHERE o.id = ?"
1087          : "WHERE o.id IN (" . join(', ', map("? ", @ids)) . ")"
1088         );
1089     @values = $form->{id} ? ($form->{id}) : @ids;
1090     $sth = prepare_execute_query($form, $dbh, $query, @values);
1091
1092     $ref = $sth->fetchrow_hashref("NAME_lc");
1093
1094     if ($ref) {
1095       map { $form->{$_} = $ref->{$_} } keys %$ref;
1096
1097       $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ? "quonumber" : "ordnumber"};
1098
1099       # set all entries for multiple ids blank that yield different information
1100       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1101         map { $form->{$_} = '' if ($ref->{$_} ne $form->{$_}) } keys %$ref;
1102       }
1103     }
1104     $form->{mtime}   ||= $form->{itime};
1105     $form->{lastmtime} = $form->{mtime};
1106
1107     # if not given, fill transdate with current_date
1108     $form->{transdate} = $form->current_date($myconfig)
1109       unless $form->{transdate};
1110
1111     $sth->finish;
1112
1113     if ($form->{delivery_customer_id}) {
1114       $query = qq|SELECT name FROM customer WHERE id = ?|;
1115       ($form->{delivery_customer_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_customer_id});
1116     }
1117
1118     if ($form->{delivery_vendor_id}) {
1119       $query = qq|SELECT name FROM customer WHERE id = ?|;
1120       ($form->{delivery_vendor_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_vendor_id});
1121     }
1122
1123     # shipto and pinted/mailed/queued status makes only sense for single id retrieve
1124     if (!@ids) {
1125       $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'OE'|;
1126       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1127
1128       $ref = $sth->fetchrow_hashref("NAME_lc");
1129       $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
1130       $sth->finish;
1131
1132       if ($ref->{shipto_id}) {
1133         my $cvars = CVar->get_custom_variables(
1134           dbh      => $dbh,
1135           module   => 'ShipTo',
1136           trans_id => $ref->{shipto_id},
1137         );
1138         $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
1139       }
1140
1141       # get printed, emailed and queued
1142       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
1143       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1144
1145       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1146         $form->{printed} .= "$ref->{formname} " if $ref->{printed};
1147         $form->{emailed} .= "$ref->{formname} " if $ref->{emailed};
1148         $form->{queued}  .= "$ref->{formname} $ref->{spoolfile} " if $ref->{spoolfile};
1149       }
1150       $sth->finish;
1151       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
1152     }    # if !@ids
1153
1154     my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) : $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
1155
1156     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
1157     unshift @values, ($form->{taxzone_id}) x 2;
1158
1159     # retrieve individual items
1160     # this query looks up all information about the items
1161     # stuff different from the whole will not be overwritten, but saved with a suffix.
1162     $query =
1163       qq|SELECT o.id AS orderitems_id,
1164            c1.accno AS inventory_accno, c1.new_chart_id AS inventory_new_chart, date($transdate) - c1.valid_from as inventory_valid,
1165            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
1166            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
1167            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
1168            p.partnumber, p.part_type, p.listprice, o.description, o.qty,
1169            p.classification_id,
1170            o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.part_type,
1171            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
1172            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
1173            o.price_factor_id, o.price_factor, o.marge_price_factor, o.active_price_source, o.active_discount_source,
1174            pr.projectnumber, p.formel,
1175            pg.partsgroup, o.pricegroup_id, (SELECT pricegroup FROM pricegroup WHERE id=o.pricegroup_id) as pricegroup
1176          FROM orderitems o
1177          JOIN parts p ON (o.parts_id = p.id)
1178          JOIN oe ON (o.trans_id = oe.id)
1179          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
1180          LEFT JOIN chart c2 ON ((SELECT tc.income_accno_id  FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c2.id)
1181          LEFT JOIN chart c3 ON ((SELECT tc.expense_accno_id FROM taxzone_charts tc WHERE tc.taxzone_id = ? and tc.buchungsgruppen_id = p.buchungsgruppen_id) = c3.id)
1182          LEFT JOIN project pr ON (o.project_id = pr.id)
1183          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1184       ($form->{id}
1185        ? qq|WHERE o.trans_id = ?|
1186        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
1187       qq|ORDER BY o.trans_id, o.position|;
1188
1189     @ids = $form->{id} ? ($form->{id}) : @ids;
1190     $sth = prepare_execute_query($form, $dbh, $query, @values);
1191
1192     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1193       # Retrieve custom variables.
1194       my $cvars = CVar->get_custom_variables(dbh        => $dbh,
1195                                              module     => 'IC',
1196                                              sub_module => 'orderitems',
1197                                              trans_id   => $ref->{orderitems_id},
1198                                             );
1199       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
1200
1201       # Handle accounts.
1202       if (!$ref->{"part_type"} eq 'part') {
1203         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
1204       }
1205       # delete($ref->{"part_inventory_accno_id"});
1206
1207       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
1208       #   unless already present there
1209       # remove _oe entries afterwards
1210       map { $ref->{$_} = $ref->{"${_}_oe"} if ($ref->{$_} eq '') }
1211         qw|ordnumber transdate cusordnumber|
1212         if (@ids);
1213       map { delete $ref->{$_} } qw|ordnumber_oe transdate_oe cusordnumber_oe|;
1214
1215
1216
1217       while ($ref->{inventory_new_chart} && ($ref->{inventory_valid} >= 0)) {
1218         my $query =
1219           qq|SELECT accno AS inventory_accno, | .
1220           qq|  new_chart_id AS inventory_new_chart, | .
1221           qq|  date($transdate) - valid_from AS inventory_valid | .
1222           qq|FROM chart WHERE id = $ref->{inventory_new_chart}|;
1223         ($ref->{inventory_accno}, $ref->{inventory_new_chart},
1224          $ref->{inventory_valid}) = selectrow_query($form, $dbh, $query);
1225       }
1226
1227       while ($ref->{income_new_chart} && ($ref->{income_valid} >= 0)) {
1228         my $query =
1229           qq|SELECT accno AS income_accno, | .
1230           qq|  new_chart_id AS income_new_chart, | .
1231           qq|  date($transdate) - valid_from AS income_valid | .
1232           qq|FROM chart WHERE id = $ref->{income_new_chart}|;
1233         ($ref->{income_accno}, $ref->{income_new_chart},
1234          $ref->{income_valid}) = selectrow_query($form, $dbh, $query);
1235       }
1236
1237       while ($ref->{expense_new_chart} && ($ref->{expense_valid} >= 0)) {
1238         my $query =
1239           qq|SELECT accno AS expense_accno, | .
1240           qq|  new_chart_id AS expense_new_chart, | .
1241           qq|  date($transdate) - valid_from AS expense_valid | .
1242           qq|FROM chart WHERE id = $ref->{expense_new_chart}|;
1243         ($ref->{expense_accno}, $ref->{expense_new_chart},
1244          $ref->{expense_valid}) = selectrow_query($form, $dbh, $query);
1245       }
1246
1247       # delete orderitems_id in collective orders, so that they get cloned no matter what
1248       # is this correct? or is the following meant?
1249       # remember orderitems_ids in converted_from_orderitems_ids, so that they may be linked
1250       $ref->{converted_from_orderitems_id} = delete $ref->{orderitems_id} if $is_collective_order;
1251
1252       # get tax rates and description
1253       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
1254       $query =
1255         qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id, c.accno as taxnumber | .
1256         qq|FROM tax t | .
1257         qq|LEFT JOIN chart c on (c.id = t.chart_id) | .
1258         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
1259         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
1260         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
1261         qq|ORDER BY c.accno|;
1262       my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
1263       $ref->{taxaccounts} = "";
1264       my $i = 0;
1265       while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
1266         if (($ptr->{accno} eq "") && ($ptr->{rate} == 0)) {
1267           $i++;
1268           $ptr->{accno} = $i;
1269         }
1270         $ref->{taxaccounts} .= "$ptr->{accno} ";
1271         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
1272           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
1273           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
1274           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
1275           $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
1276           $form->{taxaccounts} .= "$ptr->{accno} ";
1277         }
1278
1279       }
1280
1281       chop $ref->{taxaccounts};
1282
1283       push @{ $form->{form_details} }, $ref;
1284       $stw->finish;
1285     }
1286     $sth->finish;
1287
1288   } else {
1289
1290     # get last name used
1291     $form->lastname_used($dbh, $myconfig, $form->{vc})
1292       unless $form->{"$form->{vc}_id"};
1293
1294   }
1295
1296   $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? "buy" : "sell");
1297
1298   Common::webdav_folder($form);
1299
1300   $self->load_periodic_invoice_config($form);
1301
1302   return 1;
1303 }
1304
1305 sub retrieve_simple {
1306   $main::lxdebug->enter_sub();
1307
1308   my $self     = shift;
1309   my %params   = @_;
1310
1311   Common::check_params(\%params, qw(id));
1312
1313   my $myconfig    = \%main::myconfig;
1314   my $form        = $main::form;
1315
1316   my $dbh         = $params{dbh} || $form->get_standard_dbh($myconfig);
1317
1318   my $oe_query    = qq|SELECT * FROM oe         WHERE id = ?|;
1319   my $oi_query    = qq|SELECT * FROM orderitems WHERE trans_id = ? ORDER BY position|;
1320
1321   my $order            = selectfirst_hashref_query($form, $dbh, $oe_query, conv_i($params{id}));
1322   $order->{orderitems} = selectall_hashref_query(  $form, $dbh, $oi_query, conv_i($params{id}));
1323
1324   $main::lxdebug->leave_sub();
1325
1326   return $order;
1327 }
1328
1329 sub order_details {
1330   $main::lxdebug->enter_sub();
1331
1332   my ($self, $myconfig, $form) = @_;
1333
1334   # connect to database
1335   my $dbh = SL::DB->client->dbh;
1336   my $query;
1337   my @values = ();
1338   my $sth;
1339   my $nodiscount;
1340   my $yesdiscount;
1341   my $nodiscount_subtotal = 0;
1342   my $discount_subtotal = 0;
1343   my $item;
1344   my $i;
1345   my @partsgroup = ();
1346   my $partsgroup;
1347   my $position = 0;
1348   my $subtotal_header = 0;
1349   my $subposition = 0;
1350   my %taxaccounts;
1351   my %taxbase;
1352   my $tax_rate;
1353   my $taxamount;
1354
1355   my (@project_ids);
1356
1357   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
1358
1359   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
1360   my %price_factors;
1361
1362   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1363     $price_factors{$pfac->{id}}  = $pfac;
1364     $pfac->{factor}             *= 1;
1365     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
1366   }
1367
1368   # sort items by partsgroup
1369   for $i (1 .. $form->{rowcount}) {
1370     $partsgroup = "";
1371     if ($form->{"partsgroup_$i"} && $form->{groupitems}) {
1372       $partsgroup = $form->{"partsgroup_$i"};
1373     }
1374     push @partsgroup, [$i, $partsgroup];
1375     push(@project_ids, $form->{"project_id_$i"}) if ($form->{"project_id_$i"});
1376   }
1377
1378   my $projects = [];
1379   my %projects_by_id;
1380   if (@project_ids) {
1381     $projects = SL::DB::Manager::Project->get_all(query => [ id => \@project_ids ]);
1382     %projects_by_id = map { $_->id => $_ } @$projects;
1383   }
1384
1385   if ($projects_by_id{$form->{"globalproject_id"}}) {
1386     $form->{globalprojectnumber} = $projects_by_id{$form->{"globalproject_id"}}->projectnumber;
1387     $form->{globalprojectdescription} = $projects_by_id{$form->{"globalproject_id"}}->description;
1388
1389     for (@{ $projects_by_id{$form->{"globalproject_id"}}->cvars_by_config }) {
1390       $form->{"project_cvar_" . $_->config->name} = $_->value_as_text;
1391     }
1392   }
1393
1394   $form->{discount} = [];
1395
1396   # get some values of parts from db on store them in extra array,
1397   # so that they can be sorted in later
1398   my %prepared_template_arrays = IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
1399   my @prepared_arrays          = keys %prepared_template_arrays;
1400   my @separate_totals          = qw(non_separate_subtotal);
1401
1402   $form->{TEMPLATE_ARRAYS} = { };
1403
1404   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1405   my $project_cvar_configs = CVar->get_configs(module => 'Projects');
1406
1407   my @arrays =
1408     qw(runningnumber number description longdescription qty qty_nofmt ship ship_nofmt unit bin
1409        partnotes serialnumber reqdate sellprice sellprice_nofmt listprice listprice_nofmt netprice netprice_nofmt
1410        discount discount_nofmt p_discount discount_sub discount_sub_nofmt nodiscount_sub nodiscount_sub_nofmt
1411        linetotal linetotal_nofmt nodiscount_linetotal nodiscount_linetotal_nofmt tax_rate projectnumber projectdescription
1412        price_factor price_factor_name partsgroup weight weight_nofmt lineweight lineweight_nofmt optional);
1413
1414   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
1415   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
1416
1417   my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
1418
1419   map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays, @prepared_arrays);
1420
1421   my $totalweight = 0;
1422   my $sameitem = "";
1423   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
1424     $i = $item->[0];
1425
1426     if ($item->[1] ne $sameitem) {
1427       push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'partsgroup');
1428       push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, qq|$item->[1]|);
1429       $sameitem = $item->[1];
1430
1431       map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1432     }
1433
1434     $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
1435
1436     if ($form->{"id_$i"} != 0) {
1437
1438       # add number, description and qty to $form->{number}, ....
1439
1440       if ($form->{"subtotal_$i"} && !$subtotal_header) {
1441         $subtotal_header = $i;
1442         $position = int($position);
1443         $subposition = 0;
1444         $position++;
1445       } elsif ($subtotal_header) {
1446         $subposition += 1;
1447         $position = int($position);
1448         $position = $position.".".$subposition;
1449       } else {
1450         $position = int($position);
1451         $position++;
1452       }
1453
1454       my $price_factor = $price_factors{$form->{"price_factor_id_$i"}} || { 'factor' => 1 };
1455
1456       push(@{ $form->{TEMPLATE_ARRAYS}->{$_} },                $prepared_template_arrays{$_}[$i - 1]) for @prepared_arrays;
1457
1458       push @{ $form->{TEMPLATE_ARRAYS}->{entry_type} },        'normal';
1459       push @{ $form->{TEMPLATE_ARRAYS}->{runningnumber} },     $position;
1460       push @{ $form->{TEMPLATE_ARRAYS}->{number} },            $form->{"partnumber_$i"};
1461       push @{ $form->{TEMPLATE_ARRAYS}->{description} },       $form->{"description_$i"};
1462       push @{ $form->{TEMPLATE_ARRAYS}->{longdescription} },   $form->{"longdescription_$i"};
1463       push @{ $form->{TEMPLATE_ARRAYS}->{qty} },               $form->format_amount($myconfig, $form->{"qty_$i"});
1464       push @{ $form->{TEMPLATE_ARRAYS}->{qty_nofmt} },         $form->{"qty_$i"};
1465       push @{ $form->{TEMPLATE_ARRAYS}->{ship} },              $form->format_amount($myconfig, $form->{"ship_$i"});
1466       push @{ $form->{TEMPLATE_ARRAYS}->{ship_nofmt} },        $form->{"ship_$i"};
1467       push @{ $form->{TEMPLATE_ARRAYS}->{unit} },              $form->{"unit_$i"};
1468       push @{ $form->{TEMPLATE_ARRAYS}->{bin} },               $form->{"bin_$i"};
1469       push @{ $form->{TEMPLATE_ARRAYS}->{partnotes} },         $form->{"partnotes_$i"};
1470       push @{ $form->{TEMPLATE_ARRAYS}->{serialnumber} },      $form->{"serialnumber_$i"};
1471       push @{ $form->{TEMPLATE_ARRAYS}->{reqdate} },           $form->{"reqdate_$i"};
1472       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
1473       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1474       push @{ $form->{TEMPLATE_ARRAYS}->{listprice} },         $form->format_amount($myconfig, $form->{"listprice_$i"}, 2);
1475       push @{ $form->{TEMPLATE_ARRAYS}->{listprice_nofmt} },   $form->{"listprice_$i"};
1476       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
1477       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
1478       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
1479       push @{ $form->{TEMPLATE_ARRAYS}->{optional} },          $form->{"optional_$i"};
1480
1481       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1482       my ($dec)         = ($sellprice =~ /\.(\d+)/);
1483       my $decimalplaces = max 2, length($dec);
1484
1485       my $parsed_discount            = $form->parse_amount($myconfig, $form->{"discount_$i"});
1486
1487       my $linetotal_exact            = $form->{"qty_$i"} * $sellprice * (100 - $parsed_discount) / 100 / $price_factor->{factor};
1488       my $linetotal                  = $form->round_amount($linetotal_exact, 2);
1489
1490       my $nodiscount_exact_linetotal = $form->{"qty_$i"} * $sellprice                                  / $price_factor->{factor};
1491       my $nodiscount_linetotal       = $form->round_amount($nodiscount_exact_linetotal,2);
1492
1493       my $discount                   = $nodiscount_linetotal - $linetotal; # is always rounded because $nodiscount_linetotal and $linetotal are rounded
1494
1495       my $discount_round_error       = $discount + ($linetotal_exact - $nodiscount_exact_linetotal); # not used
1496
1497       $form->{"netprice_$i"}   = $form->round_amount($form->{"qty_$i"} ? ($linetotal / $form->{"qty_$i"}) : 0, $decimalplaces);
1498
1499       push @{ $form->{TEMPLATE_ARRAYS}->{netprice} },       ($form->{"netprice_$i"} != 0) ? $form->format_amount($myconfig, $form->{"netprice_$i"}, $decimalplaces) : '';
1500       push @{ $form->{TEMPLATE_ARRAYS}->{netprice_nofmt} }, ($form->{"netprice_$i"} != 0) ? $form->{"netprice_$i"} : '';
1501
1502       $linetotal = ($linetotal != 0) ? $linetotal : '';
1503
1504       push @{ $form->{TEMPLATE_ARRAYS}->{discount} },       ($discount != 0) ? $form->format_amount($myconfig, $discount * -1, 2) : '';
1505       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
1506       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
1507
1508       if ( $prepared_template_arrays{separate}[$i - 1]  ) {
1509         my $pabbr = $prepared_template_arrays{separate}[$i - 1];
1510         if ( ! $form->{"separate_${pabbr}_subtotal"} ) {
1511             push @separate_totals , "separate_${pabbr}_subtotal";
1512             $form->{"separate_${pabbr}_subtotal"} = 0;
1513         }
1514         $form->{"separate_${pabbr}_subtotal"} += $linetotal;
1515       } else {
1516         $form->{non_separate_subtotal} += $linetotal;
1517       }
1518
1519       $form->{ordtotal}         += $linetotal unless $form->{"optional_$i"};
1520       $form->{nodiscount_total} += $nodiscount_linetotal;
1521       $form->{discount_total}   += $discount;
1522
1523       if ($subtotal_header) {
1524         $discount_subtotal   += $linetotal;
1525         $nodiscount_subtotal += $nodiscount_linetotal;
1526       }
1527
1528       if ($form->{"subtotal_$i"} && $subtotal_header && ($subtotal_header != $i)) {
1529         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub} },         $form->format_amount($myconfig, $discount_subtotal,   2);
1530         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub_nofmt} },   $discount_subtotal;
1531         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub} },       $form->format_amount($myconfig, $nodiscount_subtotal, 2);
1532         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub_nofmt} }, $nodiscount_subtotal;
1533
1534         $discount_subtotal   = 0;
1535         $nodiscount_subtotal = 0;
1536         $subtotal_header     = 0;
1537
1538       } else {
1539         push @{ $form->{TEMPLATE_ARRAYS}->{$_} }, "" for qw(discount_sub nodiscount_sub discount_sub_nofmt nodiscount_sub_nofmt);
1540       }
1541
1542       if (!$form->{"discount_$i"}) {
1543         $nodiscount += $linetotal;
1544       }
1545
1546       my $project = $projects_by_id{$form->{"project_id_$i"}} || SL::DB::Project->new;
1547
1548       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal} },                  $form->format_amount($myconfig, $linetotal, 2);
1549       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal_nofmt} },            $linetotal_exact;
1550       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal} },       $form->format_amount($myconfig, $nodiscount_linetotal, 2);
1551       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal_nofmt} }, $nodiscount_linetotal;
1552       push @{ $form->{TEMPLATE_ARRAYS}->{projectnumber} },              $project->projectnumber;
1553       push @{ $form->{TEMPLATE_ARRAYS}->{projectdescription} },         $project->description;
1554
1555       my $lineweight = $form->{"qty_$i"} * $form->{"weight_$i"};
1556       $totalweight += $lineweight;
1557       push @{ $form->{TEMPLATE_ARRAYS}->{weight} },            $form->format_amount($myconfig, $form->{"weight_$i"}, 3);
1558       push @{ $form->{TEMPLATE_ARRAYS}->{weight_nofmt} },      $form->{"weight_$i"};
1559       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight} },        $form->format_amount($myconfig, $lineweight, 3);
1560       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight_nofmt} },  $lineweight;
1561
1562       my ($taxamount, $taxbase);
1563       my $taxrate = 0;
1564
1565       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
1566
1567       unless ($form->{"optional_$i"}) {
1568         if ($form->{taxincluded}) {
1569
1570           # calculate tax
1571           $taxamount = $linetotal * $taxrate / (1 + $taxrate);
1572           $taxbase = $linetotal / (1 + $taxrate);
1573         } else {
1574           $taxamount = $linetotal * $taxrate;
1575           $taxbase   = $linetotal;
1576         }
1577       }
1578
1579       if ($taxamount != 0) {
1580         foreach my $accno (split / /, $form->{"taxaccounts_$i"}) {
1581           $taxaccounts{$accno} += $taxamount * $form->{"${accno}_rate"} / $taxrate;
1582           $taxbase{$accno}     += $taxbase;
1583         }
1584       }
1585
1586       $tax_rate = $taxrate * 100;
1587       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
1588
1589       if ($form->{"part_type_$i"} eq 'assembly') {
1590         $sameitem = "";
1591
1592         # get parts and push them onto the stack
1593         my $sortorder = "";
1594         if ($form->{groupitems}) {
1595           $sortorder = qq|ORDER BY pg.partsgroup, a.position|;
1596         } else {
1597           $sortorder = qq|ORDER BY a.position|;
1598         }
1599
1600         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
1601                  qq|pg.partsgroup | .
1602                  qq|FROM assembly a | .
1603                  qq|  JOIN parts p ON (a.parts_id = p.id) | .
1604                  qq|    LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1605                  qq|    WHERE a.bom = '1' | .
1606                  qq|    AND a.id = ? | . $sortorder;
1607         @values = ($form->{"id_$i"});
1608         $sth = $dbh->prepare($query);
1609         $sth->execute(@values) || $form->dberror($query);
1610
1611         while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1612           if ($form->{groupitems} && $ref->{partsgroup} ne $sameitem) {
1613             map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1614             $sameitem = ($ref->{partsgroup}) ? $ref->{partsgroup} : "--";
1615             push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item-partsgroup');
1616             push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $sameitem);
1617           }
1618
1619           push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item');
1620           push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $form->format_amount($myconfig, $ref->{qty} * $form->{"qty_$i"}) . qq|, $ref->{partnumber}, $ref->{description}|);
1621           map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1622         }
1623         $sth->finish;
1624       }
1625
1626       CVar->get_non_editable_ic_cvars(form               => $form,
1627                                       dbh                => $dbh,
1628                                       row                => $i,
1629                                       sub_module         => 'orderitems',
1630                                       may_converted_from => ['orderitems', 'invoice']);
1631
1632       push @{ $form->{TEMPLATE_ARRAYS}->{"ic_cvar_$_->{name}"} },
1633         CVar->format_to_template(CVar->parse($form->{"ic_cvar_$_->{name}_$i"}, $_), $_)
1634           for @{ $ic_cvar_configs };
1635
1636       push @{ $form->{TEMPLATE_ARRAYS}->{"project_cvar_" . $_->config->name} }, $_->value_as_text for @{ $project->cvars_by_config };
1637     }
1638   }
1639
1640   $form->{totalweight}       = $form->format_amount($myconfig, $totalweight, 3);
1641   $form->{totalweight_nofmt} = $totalweight;
1642   my $defaults = AM->get_defaults();
1643   $form->{weightunit}        = $defaults->{weightunit};
1644
1645   my $tax = 0;
1646   foreach $item (sort keys %taxaccounts) {
1647     $tax += $taxamount = $form->round_amount($taxaccounts{$item}, 2);
1648
1649     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase} },        $form->format_amount($myconfig, $taxbase{$item}, 2));
1650     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase_nofmt} },  $taxbase{$item});
1651     push(@{ $form->{TEMPLATE_ARRAYS}->{tax} },            $form->format_amount($myconfig, $taxamount,      2));
1652     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_nofmt} },      $taxamount);
1653     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
1654     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
1655     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
1656     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_id} },         $form->{"${item}_tax_id"});
1657
1658     if ( $form->{"${item}_tax_id"} ) {
1659       my $tax_obj = SL::DB::Manager::Tax->find_by(id => $form->{"${item}_tax_id"}) or die "Can't find tax with id " . $form->{"${item}_tax_id"};
1660       my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
1661       push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
1662     }
1663   }
1664
1665   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
1666   $form->{discount_total}      = $form->format_amount($myconfig, $form->{discount_total}, 2);
1667   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
1668   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
1669
1670   if($form->{taxincluded}) {
1671     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal} - $tax, 2);
1672     $form->{subtotal_nofmt} = $form->{ordtotal} - $tax;
1673   } else {
1674     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1675     $form->{subtotal_nofmt} = $form->{ordtotal};
1676   }
1677
1678   my $grossamount = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
1679   $form->{ordtotal} = $form->round_amount( $grossamount, 2, 1);
1680   $form->{rounding} = $form->round_amount(
1681     $form->{ordtotal} - $form->round_amount($grossamount, 2),
1682     2
1683   );
1684
1685   # format amounts
1686   $form->{rounding} = $form->format_amount($myconfig, $form->{rounding}, 2);
1687   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1688
1689   $form->set_payment_options($myconfig, $form->{$form->{type} =~ /_quotation/ ? 'quodate' : 'orddate'}, $form->{type});
1690
1691   $form->{username} = $myconfig->{name};
1692
1693   $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
1694   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
1695   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
1696
1697   $form->{order} = SL::DB::Manager::Order->find_by(id => $form->{id}) if $form->{id};
1698   $form->{$_} = $form->format_amount($myconfig, $form->{$_}, 2) for @separate_totals;
1699
1700   $main::lxdebug->leave_sub();
1701 }
1702
1703 1;
1704
1705 __END__
1706
1707 =head1 NAME
1708
1709 OE.pm - Order entry module
1710
1711 =head1 DESCRIPTION
1712
1713 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>.
1714
1715 =head1 FUNCTIONS
1716
1717 =over 4
1718
1719 =item retrieve_simple PARAMS
1720
1721 simple OE retrieval by id. does not look up customer, vendor, units or any other stuff. only oe and orderitems.
1722
1723   my $order = retrieve_simple(id => 2);
1724
1725   $order => {
1726     %_OE_CONTENT,
1727     orderitems => [
1728       %_ORDERITEM_ROW_1,
1729       %_ORDERITEM_ROW_2,
1730       ...
1731     ]
1732   }
1733
1734 =back
1735
1736 =cut