Auftrag: Projekt automatisch in verknüpftem Pflichtenheft eintragen
[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   $self->_set_project_in_linked_requirement_spec(
845     type           => $form->{type},
846     project_id     => $form->{globalproject_id},
847     sales_order_id => $form->{id},
848   );
849
850   $main::lxdebug->leave_sub();
851
852   return 1;
853 }
854
855 sub _link_created_sales_order_to_requirement_specs_for_sales_quotations {
856   my ($self, %params) = @_;
857
858   # If this is a sales order created from a sales quotation and if
859   # that sales quotation was created from a requirement spec document
860   # then link the newly created sales order to the requirement spec
861   # document, too.
862
863   return if !$params{is_new};
864   return if  $params{type} ne 'sales_order';
865   return if !@{ $params{converted_from_ids} };
866
867   my $oe_objects       = SL::DB::Manager::Order->get_all(where => [ id => $params{converted_from_ids} ]);
868   my @sales_quotations = grep { $_->is_type('sales_quotation') } @{ $oe_objects };
869
870   return if !@sales_quotations;
871
872   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => [ map { $_->id } @sales_quotations ] ]);
873
874   return if !@{ $rs_orders };
875
876   $rs_orders->[0]->db->with_transaction(sub {
877     foreach my $rs_order (@{ $rs_orders }) {
878       SL::DB::RequirementSpecOrder->new(
879         order_id            => $params{sales_order_id},
880         requirement_spec_id => $rs_order->requirement_spec_id,
881         version_id          => $rs_order->version_id,
882       )->save;
883     }
884
885     1;
886   });
887 }
888
889 sub _set_project_in_linked_requirement_spec {
890   my ($self, %params) = @_;
891
892   return if  $params{type} ne 'sales_order';
893   return if !$params{project_id} || !$params{sales_order_id};
894
895   my $query = <<SQL;
896     UPDATE requirement_specs
897     SET project_id = ?
898     WHERE id IN (
899       SELECT so.requirement_spec_id
900       FROM requirement_spec_orders so
901       WHERE so.order_id = ?
902     )
903 SQL
904
905   do_query($::form, $::form->get_standard_dbh, $query, $params{project_id}, $params{sales_order_id});
906 }
907
908 sub save_periodic_invoices_config {
909   my ($self, %params) = @_;
910
911   return if !$params{oe_id};
912
913   my $config = $params{config_yaml} ? SL::YAML::Load($params{config_yaml}) : undef;
914   return if 'HASH' ne ref $config;
915
916   my $obj  = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $params{oe_id})
917           || SL::DB::PeriodicInvoicesConfig->new(oe_id => $params{oe_id});
918   $obj->update_attributes(%{ $config });
919 }
920
921 sub load_periodic_invoice_config {
922   my $self = shift;
923   my $form = shift;
924
925   delete $form->{periodic_invoices_config};
926
927   if ($form->{id}) {
928     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
929
930     if ($config_obj) {
931       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
932                                                       print printer_id copies direct_debit send_email email_recipient_contact_id email_recipient_address email_sender email_subject email_body) };
933       $form->{periodic_invoices_config} = SL::YAML::Dump($config);
934     }
935   }
936 }
937
938 sub _close_quotations_rfqs {
939   $main::lxdebug->enter_sub();
940
941   my $self     = shift;
942   my %params   = @_;
943
944   Common::check_params(\%params, qw(from_id to_id));
945
946   my $myconfig = \%main::myconfig;
947   my $form     = $main::form;
948
949   my $dbh      = $params{dbh} || SL::DB->client->dbh;
950
951   SL::DB->client->with_transaction(sub {
952
953     my $query    = qq|SELECT quotation FROM oe WHERE id = ?|;
954     my $sth      = prepare_query($form, $dbh, $query);
955
956     do_statement($form, $sth, $query, conv_i($params{to_id}));
957
958     my ($quotation) = $sth->fetchrow_array();
959
960     if ($quotation) {
961       return 1;
962     }
963
964     my @close_ids;
965
966     foreach my $from_id (@{ $params{from_id} }) {
967       $from_id = conv_i($from_id);
968       do_statement($form, $sth, $query, $from_id);
969       ($quotation) = $sth->fetchrow_array();
970       push @close_ids, $from_id if ($quotation);
971     }
972
973     $sth->finish();
974
975     if (scalar @close_ids) {
976       $query = qq|UPDATE oe SET closed = TRUE WHERE id IN (| . join(', ', ('?') x scalar @close_ids) . qq|)|;
977       do_query($form, $dbh, $query, @close_ids);
978     }
979     1;
980   }) or do { die SL::DB->client->error };
981
982   $main::lxdebug->leave_sub();
983 }
984
985 sub delete {
986   $main::lxdebug->enter_sub();
987
988   my ($self, $myconfig, $form) = @_;
989
990   my $rc = SL::DB::Order->new->db->with_transaction(sub {
991     my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $form->{id} ]) };
992
993     SL::DB::Order->new(id => $form->{id})->delete;
994
995     my $spool = $::lx_office_conf{paths}->{spool};
996     unlink map { "$spool/$_" } @spoolfiles if $spool;
997
998     1;
999   }) or do { die SL::DB->client->error };
1000
1001   $main::lxdebug->leave_sub();
1002
1003   return $rc;
1004 }
1005
1006 sub retrieve {
1007   my ($self, $myconfig, $form) = @_;
1008   $main::lxdebug->enter_sub();
1009
1010   my $rc = SL::DB->client->with_transaction(\&_retrieve, $self, $myconfig, $form);
1011
1012   $::lxdebug->leave_sub;
1013   return $rc;
1014 }
1015
1016 sub _retrieve {
1017   my ($self, $myconfig, $form) = @_;
1018
1019   # connect to database
1020   my $dbh = SL::DB->client->dbh;
1021
1022   my ($query, $query_add, @values, @ids, $sth);
1023
1024   # translate the ids (given by id_# and trans_id_#) into one array of ids, so we can join them later
1025   map {
1026     push @ids, $form->{"trans_id_$_"}
1027       if ($form->{"multi_id_$_"} and $form->{"trans_id_$_"})
1028   } (1 .. $form->{"rowcount"});
1029
1030   if ($form->{rowcount} && scalar @ids) {
1031     $form->{convert_from_oe_ids} = join ' ', @ids;
1032   }
1033
1034   # if called in multi id mode, and still only got one id, switch back to single id
1035   if ($form->{"rowcount"} and $#ids == 0) {
1036     $form->{"id"} = $ids[0];
1037     undef @ids;
1038     delete $form->{convert_from_oe_ids};
1039   }
1040
1041   # and remember for the rest of the function
1042   my $is_collective_order = scalar @ids;
1043
1044   # If collective order was created from exactly 1 order, we assume the same
1045   # behaviour as a "save as new" from within an order is actually desired, i.e.
1046   # the original order isn't part of a workflow where we want to remember
1047   # record_links, but simply a quick way of generating a new order from an old
1048   # one without having to enter everything again.
1049   # Setting useasnew will prevent the creation of record_links for the items
1050   # when saving the new order.
1051   # This form variable is probably not necessary, could just set saveasnew instead
1052   $form->{useasnew} = 1 if $is_collective_order == 1;
1053
1054   if (!$form->{id}) {
1055     my $extra_days = $form->{type} eq 'sales_quotation' ? $::instance_conf->get_reqdate_interval       :
1056                      $form->{type} eq 'sales_order'     ? $::instance_conf->get_delivery_date_interval : 1;
1057     if (   ($form->{type} eq 'sales_order'     &&  !$::instance_conf->get_deliverydate_on)
1058         || ($form->{type} eq 'sales_quotation' &&  !$::instance_conf->get_reqdate_on)) {
1059       $form->{reqdate}   = '';
1060     } else {
1061       $form->{reqdate}   = DateTime->today_local->next_workday(extra_days => $extra_days)->to_kivitendo;
1062     }
1063     $form->{transdate} = DateTime->today_local->to_kivitendo;
1064   }
1065
1066   # get default accounts
1067   $query = qq|SELECT (SELECT c.accno FROM chart c WHERE d.inventory_accno_id = c.id) AS inventory_accno,
1068                      (SELECT c.accno FROM chart c WHERE d.income_accno_id    = c.id) AS income_accno,
1069                      (SELECT c.accno FROM chart c WHERE d.expense_accno_id   = c.id) AS expense_accno,
1070                      (SELECT c.accno FROM chart c WHERE d.fxgain_accno_id    = c.id) AS fxgain_accno,
1071                      (SELECT c.accno FROM chart c WHERE d.fxloss_accno_id    = c.id) AS fxloss_accno,
1072                      (SELECT c.accno FROM chart c WHERE d.rndgain_accno_id   = c.id) AS rndgain_accno,
1073                      (SELECT c.accno FROM chart c WHERE d.rndloss_accno_id   = c.id) AS rndloss_accno
1074               $query_add
1075               FROM defaults d|;
1076   my $ref = selectfirst_hashref_query($form, $dbh, $query);
1077   map { $form->{$_} = $ref->{$_} } keys %$ref;
1078
1079   $form->{currency} = $form->get_default_currency($myconfig);
1080
1081   # set reqdate if this is an invoice->order conversion. If someone knows a better check to ensure
1082   # we come from invoices, feel free.
1083   $form->{reqdate} = $form->{deliverydate}
1084     if (    $form->{deliverydate}
1085         and $form->{callback} =~ /action=ar_transactions/);
1086
1087   my $vc = $form->{vc} eq "customer" ? "customer" : "vendor";
1088
1089   if ($form->{id} or @ids) {
1090
1091     # retrieve order for single id
1092     # NOTE: this query is intended to fetch all information only ONCE.
1093     # so if any of these infos is important (or even different) for any item,
1094     # it will be killed out and then has to be fetched from the item scope query further down
1095     $query =
1096       qq|SELECT o.cp_id, o.ordnumber, o.transdate, o.reqdate,
1097            o.taxincluded, o.shippingpoint, o.shipvia, o.notes, o.intnotes,
1098            (SELECT cu.name FROM currencies cu WHERE cu.id=o.currency_id) AS currency, e.name AS employee, o.employee_id, o.salesman_id,
1099            o.${vc}_id, cv.name AS ${vc}, o.amount AS invtotal,
1100            o.closed, o.reqdate, o.tax_point, o.quonumber, o.department_id, o.cusordnumber,
1101            o.mtime, o.itime,
1102            d.description AS department, o.payment_id, o.language_id, o.taxzone_id,
1103            o.delivery_customer_id, o.delivery_vendor_id, o.proforma, o.shipto_id,
1104            o.globalproject_id, o.delivered, o.transaction_description, o.delivery_term_id,
1105            o.itime::DATE AS insertdate, o.order_probability, o.expected_billing_date
1106          FROM oe o
1107          JOIN ${vc} cv ON (o.${vc}_id = cv.id)
1108          LEFT JOIN employee e ON (o.employee_id = e.id)
1109          LEFT JOIN department d ON (o.department_id = d.id) | .
1110         ($form->{id}
1111          ? "WHERE o.id = ?"
1112          : "WHERE o.id IN (" . join(', ', map("? ", @ids)) . ")"
1113         );
1114     @values = $form->{id} ? ($form->{id}) : @ids;
1115     $sth = prepare_execute_query($form, $dbh, $query, @values);
1116
1117     $ref = $sth->fetchrow_hashref("NAME_lc");
1118
1119     if ($ref) {
1120       map { $form->{$_} = $ref->{$_} } keys %$ref;
1121
1122       $form->{saved_xyznumber} = $form->{$form->{type} =~ /_quotation$/ ? "quonumber" : "ordnumber"};
1123
1124       # set all entries for multiple ids blank that yield different information
1125       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1126         map { $form->{$_} = '' if ($ref->{$_} ne $form->{$_}) } keys %$ref;
1127       }
1128     }
1129     $form->{mtime}   ||= $form->{itime};
1130     $form->{lastmtime} = $form->{mtime};
1131
1132     # if not given, fill transdate with current_date
1133     $form->{transdate} = $form->current_date($myconfig)
1134       unless $form->{transdate};
1135
1136     $sth->finish;
1137
1138     if ($form->{delivery_customer_id}) {
1139       $query = qq|SELECT name FROM customer WHERE id = ?|;
1140       ($form->{delivery_customer_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_customer_id});
1141     }
1142
1143     if ($form->{delivery_vendor_id}) {
1144       $query = qq|SELECT name FROM customer WHERE id = ?|;
1145       ($form->{delivery_vendor_string}) = selectrow_query($form, $dbh, $query, $form->{delivery_vendor_id});
1146     }
1147
1148     # shipto and pinted/mailed/queued status makes only sense for single id retrieve
1149     if (!@ids) {
1150       $query = qq|SELECT s.* FROM shipto s WHERE s.trans_id = ? AND s.module = 'OE'|;
1151       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1152
1153       $ref = $sth->fetchrow_hashref("NAME_lc");
1154       $form->{$_} = $ref->{$_} for grep { m{^shipto(?!_id$)} } keys %$ref;
1155       $sth->finish;
1156
1157       if ($ref->{shipto_id}) {
1158         my $cvars = CVar->get_custom_variables(
1159           dbh      => $dbh,
1160           module   => 'ShipTo',
1161           trans_id => $ref->{shipto_id},
1162         );
1163         $form->{"shiptocvar_$_->{name}"} = $_->{value} for @{ $cvars };
1164       }
1165
1166       # get printed, emailed and queued
1167       $query = qq|SELECT s.printed, s.emailed, s.spoolfile, s.formname FROM status s WHERE s.trans_id = ?|;
1168       $sth = prepare_execute_query($form, $dbh, $query, $form->{id});
1169
1170       while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1171         $form->{printed} .= "$ref->{formname} " if $ref->{printed};
1172         $form->{emailed} .= "$ref->{formname} " if $ref->{emailed};
1173         $form->{queued}  .= "$ref->{formname} $ref->{spoolfile} " if $ref->{spoolfile};
1174       }
1175       $sth->finish;
1176       map { $form->{$_} =~ s/ +$//g } qw(printed emailed queued);
1177     }    # if !@ids
1178
1179     my $transdate = $form->{tax_point} ? $dbh->quote($form->{tax_point}) : $form->{transdate} ? $dbh->quote($form->{transdate}) : "current_date";
1180
1181     $form->{taxzone_id} = 0 unless ($form->{taxzone_id});
1182     unshift @values, ($form->{taxzone_id}) x 2;
1183
1184     # retrieve individual items
1185     # this query looks up all information about the items
1186     # stuff different from the whole will not be overwritten, but saved with a suffix.
1187     $query =
1188       qq|SELECT o.id AS orderitems_id,
1189            c1.accno AS inventory_accno, c1.new_chart_id AS inventory_new_chart, date($transdate) - c1.valid_from as inventory_valid,
1190            c2.accno AS income_accno,    c2.new_chart_id AS income_new_chart,    date($transdate) - c2.valid_from as income_valid,
1191            c3.accno AS expense_accno,   c3.new_chart_id AS expense_new_chart,   date($transdate) - c3.valid_from as expense_valid,
1192            oe.ordnumber AS ordnumber_oe, oe.transdate AS transdate_oe, oe.cusordnumber AS cusordnumber_oe,
1193            p.partnumber, p.part_type, p.listprice, o.description, o.qty,
1194            p.classification_id,
1195            o.sellprice, o.parts_id AS id, o.unit, o.discount, p.notes AS partnotes, p.part_type,
1196            o.reqdate, o.project_id, o.serialnumber, o.ship, o.lastcost,
1197            o.ordnumber, o.transdate, o.cusordnumber, o.subtotal, o.longdescription,
1198            o.price_factor_id, o.price_factor, o.marge_price_factor, o.active_price_source, o.active_discount_source,
1199            pr.projectnumber, p.formel,
1200            pg.partsgroup, o.pricegroup_id, (SELECT pricegroup FROM pricegroup WHERE id=o.pricegroup_id) as pricegroup
1201          FROM orderitems o
1202          JOIN parts p ON (o.parts_id = p.id)
1203          JOIN oe ON (o.trans_id = oe.id)
1204          LEFT JOIN chart c1 ON ((SELECT inventory_accno_id                   FROM buchungsgruppen WHERE id=p.buchungsgruppen_id) = c1.id)
1205          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)
1206          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)
1207          LEFT JOIN project pr ON (o.project_id = pr.id)
1208          LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1209       ($form->{id}
1210        ? qq|WHERE o.trans_id = ?|
1211        : qq|WHERE o.trans_id IN (| . join(", ", map("?", @ids)) . qq|)|) .
1212       qq|ORDER BY o.trans_id, o.position|;
1213
1214     @ids = $form->{id} ? ($form->{id}) : @ids;
1215     $sth = prepare_execute_query($form, $dbh, $query, @values);
1216
1217     while ($ref = $sth->fetchrow_hashref("NAME_lc")) {
1218       # Retrieve custom variables.
1219       my $cvars = CVar->get_custom_variables(dbh        => $dbh,
1220                                              module     => 'IC',
1221                                              sub_module => 'orderitems',
1222                                              trans_id   => $ref->{orderitems_id},
1223                                             );
1224       map { $ref->{"ic_cvar_$_->{name}"} = $_->{value} } @{ $cvars };
1225
1226       # Handle accounts.
1227       if (!$ref->{"part_type"} eq 'part') {
1228         map({ delete($ref->{$_}); } qw(inventory_accno inventory_new_chart inventory_valid));
1229       }
1230       # delete($ref->{"part_inventory_accno_id"});
1231
1232       # in collective order, copy global ordnumber, transdate, cusordnumber into item scope
1233       #   unless already present there
1234       # remove _oe entries afterwards
1235       map { $ref->{$_} = $ref->{"${_}_oe"} if ($ref->{$_} eq '') }
1236         qw|ordnumber transdate cusordnumber|
1237         if (@ids);
1238       map { delete $ref->{$_} } qw|ordnumber_oe transdate_oe cusordnumber_oe|;
1239
1240
1241
1242       while ($ref->{inventory_new_chart} && ($ref->{inventory_valid} >= 0)) {
1243         my $query =
1244           qq|SELECT accno AS inventory_accno, | .
1245           qq|  new_chart_id AS inventory_new_chart, | .
1246           qq|  date($transdate) - valid_from AS inventory_valid | .
1247           qq|FROM chart WHERE id = $ref->{inventory_new_chart}|;
1248         ($ref->{inventory_accno}, $ref->{inventory_new_chart},
1249          $ref->{inventory_valid}) = selectrow_query($form, $dbh, $query);
1250       }
1251
1252       while ($ref->{income_new_chart} && ($ref->{income_valid} >= 0)) {
1253         my $query =
1254           qq|SELECT accno AS income_accno, | .
1255           qq|  new_chart_id AS income_new_chart, | .
1256           qq|  date($transdate) - valid_from AS income_valid | .
1257           qq|FROM chart WHERE id = $ref->{income_new_chart}|;
1258         ($ref->{income_accno}, $ref->{income_new_chart},
1259          $ref->{income_valid}) = selectrow_query($form, $dbh, $query);
1260       }
1261
1262       while ($ref->{expense_new_chart} && ($ref->{expense_valid} >= 0)) {
1263         my $query =
1264           qq|SELECT accno AS expense_accno, | .
1265           qq|  new_chart_id AS expense_new_chart, | .
1266           qq|  date($transdate) - valid_from AS expense_valid | .
1267           qq|FROM chart WHERE id = $ref->{expense_new_chart}|;
1268         ($ref->{expense_accno}, $ref->{expense_new_chart},
1269          $ref->{expense_valid}) = selectrow_query($form, $dbh, $query);
1270       }
1271
1272       # delete orderitems_id in collective orders, so that they get cloned no matter what
1273       # is this correct? or is the following meant?
1274       # remember orderitems_ids in converted_from_orderitems_ids, so that they may be linked
1275       $ref->{converted_from_orderitems_id} = delete $ref->{orderitems_id} if $is_collective_order;
1276
1277       # get tax rates and description
1278       my $accno_id = ($form->{vc} eq "customer") ? $ref->{income_accno} : $ref->{expense_accno};
1279       $query =
1280         qq|SELECT c.accno, t.taxdescription, t.rate, t.id as tax_id, c.accno as taxnumber | .
1281         qq|FROM tax t | .
1282         qq|LEFT JOIN chart c on (c.id = t.chart_id) | .
1283         qq|WHERE t.id IN (SELECT tk.tax_id FROM taxkeys tk | .
1284         qq|               WHERE tk.chart_id = (SELECT id FROM chart WHERE accno = ?) | .
1285         qq|                 AND startdate <= $transdate ORDER BY startdate DESC LIMIT 1) | .
1286         qq|ORDER BY c.accno|;
1287       my $stw = prepare_execute_query($form, $dbh, $query, $accno_id);
1288       $ref->{taxaccounts} = "";
1289       my $i = 0;
1290       while (my $ptr = $stw->fetchrow_hashref("NAME_lc")) {
1291         if (($ptr->{accno} eq "") && ($ptr->{rate} == 0)) {
1292           $i++;
1293           $ptr->{accno} = $i;
1294         }
1295         $ref->{taxaccounts} .= "$ptr->{accno} ";
1296         if (!($form->{taxaccounts} =~ /\Q$ptr->{accno}\E/)) {
1297           $form->{"$ptr->{accno}_rate"}        = $ptr->{rate};
1298           $form->{"$ptr->{accno}_description"} = $ptr->{taxdescription};
1299           $form->{"$ptr->{accno}_taxnumber"}   = $ptr->{taxnumber};
1300           $form->{"$ptr->{accno}_tax_id"}      = $ptr->{tax_id};
1301           $form->{taxaccounts} .= "$ptr->{accno} ";
1302         }
1303
1304       }
1305
1306       chop $ref->{taxaccounts};
1307
1308       push @{ $form->{form_details} }, $ref;
1309       $stw->finish;
1310     }
1311     $sth->finish;
1312
1313   } else {
1314
1315     # get last name used
1316     $form->lastname_used($dbh, $myconfig, $form->{vc})
1317       unless $form->{"$form->{vc}_id"};
1318
1319   }
1320
1321   $form->{exchangerate} = $form->get_exchangerate($dbh, $form->{currency}, $form->{transdate}, ($form->{vc} eq 'customer') ? "buy" : "sell");
1322
1323   Common::webdav_folder($form);
1324
1325   $self->load_periodic_invoice_config($form);
1326
1327   return 1;
1328 }
1329
1330 sub retrieve_simple {
1331   $main::lxdebug->enter_sub();
1332
1333   my $self     = shift;
1334   my %params   = @_;
1335
1336   Common::check_params(\%params, qw(id));
1337
1338   my $myconfig    = \%main::myconfig;
1339   my $form        = $main::form;
1340
1341   my $dbh         = $params{dbh} || $form->get_standard_dbh($myconfig);
1342
1343   my $oe_query    = qq|SELECT * FROM oe         WHERE id = ?|;
1344   my $oi_query    = qq|SELECT * FROM orderitems WHERE trans_id = ? ORDER BY position|;
1345
1346   my $order            = selectfirst_hashref_query($form, $dbh, $oe_query, conv_i($params{id}));
1347   $order->{orderitems} = selectall_hashref_query(  $form, $dbh, $oi_query, conv_i($params{id}));
1348
1349   $main::lxdebug->leave_sub();
1350
1351   return $order;
1352 }
1353
1354 sub order_details {
1355   $main::lxdebug->enter_sub();
1356
1357   my ($self, $myconfig, $form) = @_;
1358
1359   # connect to database
1360   my $dbh = SL::DB->client->dbh;
1361   my $query;
1362   my @values = ();
1363   my $sth;
1364   my $nodiscount;
1365   my $yesdiscount;
1366   my $nodiscount_subtotal = 0;
1367   my $discount_subtotal = 0;
1368   my $item;
1369   my $i;
1370   my @partsgroup = ();
1371   my $partsgroup;
1372   my $position = 0;
1373   my $subtotal_header = 0;
1374   my $subposition = 0;
1375   my %taxaccounts;
1376   my %taxbase;
1377   my $tax_rate;
1378   my $taxamount;
1379
1380   my (@project_ids);
1381
1382   push(@project_ids, $form->{"globalproject_id"}) if ($form->{"globalproject_id"});
1383
1384   $form->get_lists('price_factors' => 'ALL_PRICE_FACTORS');
1385   my %price_factors;
1386
1387   foreach my $pfac (@{ $form->{ALL_PRICE_FACTORS} }) {
1388     $price_factors{$pfac->{id}}  = $pfac;
1389     $pfac->{factor}             *= 1;
1390     $pfac->{formatted_factor}    = $form->format_amount($myconfig, $pfac->{factor});
1391   }
1392
1393   # sort items by partsgroup
1394   for $i (1 .. $form->{rowcount}) {
1395     $partsgroup = "";
1396     if ($form->{"partsgroup_$i"} && $form->{groupitems}) {
1397       $partsgroup = $form->{"partsgroup_$i"};
1398     }
1399     push @partsgroup, [$i, $partsgroup];
1400     push(@project_ids, $form->{"project_id_$i"}) if ($form->{"project_id_$i"});
1401   }
1402
1403   my $projects = [];
1404   my %projects_by_id;
1405   if (@project_ids) {
1406     $projects = SL::DB::Manager::Project->get_all(query => [ id => \@project_ids ]);
1407     %projects_by_id = map { $_->id => $_ } @$projects;
1408   }
1409
1410   if ($projects_by_id{$form->{"globalproject_id"}}) {
1411     $form->{globalprojectnumber} = $projects_by_id{$form->{"globalproject_id"}}->projectnumber;
1412     $form->{globalprojectdescription} = $projects_by_id{$form->{"globalproject_id"}}->description;
1413
1414     for (@{ $projects_by_id{$form->{"globalproject_id"}}->cvars_by_config }) {
1415       $form->{"project_cvar_" . $_->config->name} = $_->value_as_text;
1416     }
1417   }
1418
1419   $form->{discount} = [];
1420
1421   # get some values of parts from db on store them in extra array,
1422   # so that they can be sorted in later
1423   my %prepared_template_arrays = IC->prepare_parts_for_printing(myconfig => $myconfig, form => $form);
1424   my @prepared_arrays          = keys %prepared_template_arrays;
1425   my @separate_totals          = qw(non_separate_subtotal);
1426
1427   $form->{TEMPLATE_ARRAYS} = { };
1428
1429   my $ic_cvar_configs = CVar->get_configs(module => 'IC');
1430   my $project_cvar_configs = CVar->get_configs(module => 'Projects');
1431
1432   my @arrays =
1433     qw(runningnumber number description longdescription qty qty_nofmt ship ship_nofmt unit bin
1434        partnotes serialnumber reqdate sellprice sellprice_nofmt listprice listprice_nofmt netprice netprice_nofmt
1435        discount discount_nofmt p_discount discount_sub discount_sub_nofmt nodiscount_sub nodiscount_sub_nofmt
1436        linetotal linetotal_nofmt nodiscount_linetotal nodiscount_linetotal_nofmt tax_rate projectnumber projectdescription
1437        price_factor price_factor_name partsgroup weight weight_nofmt lineweight lineweight_nofmt optional);
1438
1439   push @arrays, map { "ic_cvar_$_->{name}" } @{ $ic_cvar_configs };
1440   push @arrays, map { "project_cvar_$_->{name}" } @{ $project_cvar_configs };
1441
1442   my @tax_arrays = qw(taxbase tax taxdescription taxrate taxnumber);
1443
1444   map { $form->{TEMPLATE_ARRAYS}->{$_} = [] } (@arrays, @tax_arrays, @prepared_arrays);
1445
1446   my $totalweight = 0;
1447   my $sameitem = "";
1448   foreach $item (sort { $a->[1] cmp $b->[1] } @partsgroup) {
1449     $i = $item->[0];
1450
1451     if ($item->[1] ne $sameitem) {
1452       push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'partsgroup');
1453       push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, qq|$item->[1]|);
1454       $sameitem = $item->[1];
1455
1456       map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1457     }
1458
1459     $form->{"qty_$i"} = $form->parse_amount($myconfig, $form->{"qty_$i"});
1460
1461     if ($form->{"id_$i"} != 0) {
1462
1463       # add number, description and qty to $form->{number}, ....
1464
1465       if ($form->{"subtotal_$i"} && !$subtotal_header) {
1466         $subtotal_header = $i;
1467         $position = int($position);
1468         $subposition = 0;
1469         $position++;
1470       } elsif ($subtotal_header) {
1471         $subposition += 1;
1472         $position = int($position);
1473         $position = $position.".".$subposition;
1474       } else {
1475         $position = int($position);
1476         $position++;
1477       }
1478
1479       my $price_factor = $price_factors{$form->{"price_factor_id_$i"}} || { 'factor' => 1 };
1480
1481       push(@{ $form->{TEMPLATE_ARRAYS}->{$_} },                $prepared_template_arrays{$_}[$i - 1]) for @prepared_arrays;
1482
1483       push @{ $form->{TEMPLATE_ARRAYS}->{entry_type} },        'normal';
1484       push @{ $form->{TEMPLATE_ARRAYS}->{runningnumber} },     $position;
1485       push @{ $form->{TEMPLATE_ARRAYS}->{number} },            $form->{"partnumber_$i"};
1486       push @{ $form->{TEMPLATE_ARRAYS}->{description} },       $form->{"description_$i"};
1487       push @{ $form->{TEMPLATE_ARRAYS}->{longdescription} },   $form->{"longdescription_$i"};
1488       push @{ $form->{TEMPLATE_ARRAYS}->{qty} },               $form->format_amount($myconfig, $form->{"qty_$i"});
1489       push @{ $form->{TEMPLATE_ARRAYS}->{qty_nofmt} },         $form->{"qty_$i"};
1490       push @{ $form->{TEMPLATE_ARRAYS}->{ship} },              $form->format_amount($myconfig, $form->{"ship_$i"});
1491       push @{ $form->{TEMPLATE_ARRAYS}->{ship_nofmt} },        $form->{"ship_$i"};
1492       push @{ $form->{TEMPLATE_ARRAYS}->{unit} },              $form->{"unit_$i"};
1493       push @{ $form->{TEMPLATE_ARRAYS}->{bin} },               $form->{"bin_$i"};
1494       push @{ $form->{TEMPLATE_ARRAYS}->{partnotes} },         $form->{"partnotes_$i"};
1495       push @{ $form->{TEMPLATE_ARRAYS}->{serialnumber} },      $form->{"serialnumber_$i"};
1496       push @{ $form->{TEMPLATE_ARRAYS}->{reqdate} },           $form->{"reqdate_$i"};
1497       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice} },         $form->{"sellprice_$i"};
1498       push @{ $form->{TEMPLATE_ARRAYS}->{sellprice_nofmt} },   $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1499       push @{ $form->{TEMPLATE_ARRAYS}->{listprice} },         $form->format_amount($myconfig, $form->{"listprice_$i"}, 2);
1500       push @{ $form->{TEMPLATE_ARRAYS}->{listprice_nofmt} },   $form->{"listprice_$i"};
1501       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor} },      $price_factor->{formatted_factor};
1502       push @{ $form->{TEMPLATE_ARRAYS}->{price_factor_name} }, $price_factor->{description};
1503       push @{ $form->{TEMPLATE_ARRAYS}->{partsgroup} },        $form->{"partsgroup_$i"};
1504       push @{ $form->{TEMPLATE_ARRAYS}->{optional} },          $form->{"optional_$i"};
1505
1506       my $sellprice     = $form->parse_amount($myconfig, $form->{"sellprice_$i"});
1507       my ($dec)         = ($sellprice =~ /\.(\d+)/);
1508       my $decimalplaces = max 2, length($dec);
1509
1510       my $parsed_discount            = $form->parse_amount($myconfig, $form->{"discount_$i"});
1511
1512       my $linetotal_exact            = $form->{"qty_$i"} * $sellprice * (100 - $parsed_discount) / 100 / $price_factor->{factor};
1513       my $linetotal                  = $form->round_amount($linetotal_exact, 2);
1514
1515       my $nodiscount_exact_linetotal = $form->{"qty_$i"} * $sellprice                                  / $price_factor->{factor};
1516       my $nodiscount_linetotal       = $form->round_amount($nodiscount_exact_linetotal,2);
1517
1518       my $discount                   = $nodiscount_linetotal - $linetotal; # is always rounded because $nodiscount_linetotal and $linetotal are rounded
1519
1520       my $discount_round_error       = $discount + ($linetotal_exact - $nodiscount_exact_linetotal); # not used
1521
1522       $form->{"netprice_$i"}   = $form->round_amount($form->{"qty_$i"} ? ($linetotal / $form->{"qty_$i"}) : 0, $decimalplaces);
1523
1524       push @{ $form->{TEMPLATE_ARRAYS}->{netprice} },       ($form->{"netprice_$i"} != 0) ? $form->format_amount($myconfig, $form->{"netprice_$i"}, $decimalplaces) : '';
1525       push @{ $form->{TEMPLATE_ARRAYS}->{netprice_nofmt} }, ($form->{"netprice_$i"} != 0) ? $form->{"netprice_$i"} : '';
1526
1527       $linetotal = ($linetotal != 0) ? $linetotal : '';
1528
1529       push @{ $form->{TEMPLATE_ARRAYS}->{discount} },       ($discount != 0) ? $form->format_amount($myconfig, $discount * -1, 2) : '';
1530       push @{ $form->{TEMPLATE_ARRAYS}->{discount_nofmt} }, ($discount != 0) ? $discount * -1 : '';
1531       push @{ $form->{TEMPLATE_ARRAYS}->{p_discount} },     $form->{"discount_$i"};
1532
1533       if ( $prepared_template_arrays{separate}[$i - 1]  ) {
1534         my $pabbr = $prepared_template_arrays{separate}[$i - 1];
1535         if ( ! $form->{"separate_${pabbr}_subtotal"} ) {
1536             push @separate_totals , "separate_${pabbr}_subtotal";
1537             $form->{"separate_${pabbr}_subtotal"} = 0;
1538         }
1539         $form->{"separate_${pabbr}_subtotal"} += $linetotal;
1540       } else {
1541         $form->{non_separate_subtotal} += $linetotal;
1542       }
1543
1544       $form->{ordtotal}         += $linetotal unless $form->{"optional_$i"};
1545       $form->{nodiscount_total} += $nodiscount_linetotal;
1546       $form->{discount_total}   += $discount;
1547
1548       if ($subtotal_header) {
1549         $discount_subtotal   += $linetotal;
1550         $nodiscount_subtotal += $nodiscount_linetotal;
1551       }
1552
1553       if ($form->{"subtotal_$i"} && $subtotal_header && ($subtotal_header != $i)) {
1554         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub} },         $form->format_amount($myconfig, $discount_subtotal,   2);
1555         push @{ $form->{TEMPLATE_ARRAYS}->{discount_sub_nofmt} },   $discount_subtotal;
1556         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub} },       $form->format_amount($myconfig, $nodiscount_subtotal, 2);
1557         push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_sub_nofmt} }, $nodiscount_subtotal;
1558
1559         $discount_subtotal   = 0;
1560         $nodiscount_subtotal = 0;
1561         $subtotal_header     = 0;
1562
1563       } else {
1564         push @{ $form->{TEMPLATE_ARRAYS}->{$_} }, "" for qw(discount_sub nodiscount_sub discount_sub_nofmt nodiscount_sub_nofmt);
1565       }
1566
1567       if (!$form->{"discount_$i"}) {
1568         $nodiscount += $linetotal;
1569       }
1570
1571       my $project = $projects_by_id{$form->{"project_id_$i"}} || SL::DB::Project->new;
1572
1573       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal} },                  $form->format_amount($myconfig, $linetotal, 2);
1574       push @{ $form->{TEMPLATE_ARRAYS}->{linetotal_nofmt} },            $linetotal_exact;
1575       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal} },       $form->format_amount($myconfig, $nodiscount_linetotal, 2);
1576       push @{ $form->{TEMPLATE_ARRAYS}->{nodiscount_linetotal_nofmt} }, $nodiscount_linetotal;
1577       push @{ $form->{TEMPLATE_ARRAYS}->{projectnumber} },              $project->projectnumber;
1578       push @{ $form->{TEMPLATE_ARRAYS}->{projectdescription} },         $project->description;
1579
1580       my $lineweight = $form->{"qty_$i"} * $form->{"weight_$i"};
1581       $totalweight += $lineweight;
1582       push @{ $form->{TEMPLATE_ARRAYS}->{weight} },            $form->format_amount($myconfig, $form->{"weight_$i"}, 3);
1583       push @{ $form->{TEMPLATE_ARRAYS}->{weight_nofmt} },      $form->{"weight_$i"};
1584       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight} },        $form->format_amount($myconfig, $lineweight, 3);
1585       push @{ $form->{TEMPLATE_ARRAYS}->{lineweight_nofmt} },  $lineweight;
1586
1587       my ($taxamount, $taxbase);
1588       my $taxrate = 0;
1589
1590       map { $taxrate += $form->{"${_}_rate"} } split(/ /, $form->{"taxaccounts_$i"});
1591
1592       unless ($form->{"optional_$i"}) {
1593         if ($form->{taxincluded}) {
1594
1595           # calculate tax
1596           $taxamount = $linetotal * $taxrate / (1 + $taxrate);
1597           $taxbase = $linetotal / (1 + $taxrate);
1598         } else {
1599           $taxamount = $linetotal * $taxrate;
1600           $taxbase   = $linetotal;
1601         }
1602       }
1603
1604       if ($taxamount != 0) {
1605         foreach my $accno (split / /, $form->{"taxaccounts_$i"}) {
1606           $taxaccounts{$accno} += $taxamount * $form->{"${accno}_rate"} / $taxrate;
1607           $taxbase{$accno}     += $taxbase;
1608         }
1609       }
1610
1611       $tax_rate = $taxrate * 100;
1612       push(@{ $form->{TEMPLATE_ARRAYS}->{tax_rate} }, qq|$tax_rate|);
1613
1614       if ($form->{"part_type_$i"} eq 'assembly') {
1615         $sameitem = "";
1616
1617         # get parts and push them onto the stack
1618         my $sortorder = "";
1619         if ($form->{groupitems}) {
1620           $sortorder = qq|ORDER BY pg.partsgroup, a.position|;
1621         } else {
1622           $sortorder = qq|ORDER BY a.position|;
1623         }
1624
1625         $query = qq|SELECT p.partnumber, p.description, p.unit, a.qty, | .
1626                  qq|pg.partsgroup | .
1627                  qq|FROM assembly a | .
1628                  qq|  JOIN parts p ON (a.parts_id = p.id) | .
1629                  qq|    LEFT JOIN partsgroup pg ON (p.partsgroup_id = pg.id) | .
1630                  qq|    WHERE a.bom = '1' | .
1631                  qq|    AND a.id = ? | . $sortorder;
1632         @values = ($form->{"id_$i"});
1633         $sth = $dbh->prepare($query);
1634         $sth->execute(@values) || $form->dberror($query);
1635
1636         while (my $ref = $sth->fetchrow_hashref("NAME_lc")) {
1637           if ($form->{groupitems} && $ref->{partsgroup} ne $sameitem) {
1638             map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1639             $sameitem = ($ref->{partsgroup}) ? $ref->{partsgroup} : "--";
1640             push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item-partsgroup');
1641             push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $sameitem);
1642           }
1643
1644           push(@{ $form->{TEMPLATE_ARRAYS}->{entry_type}  }, 'assembly-item');
1645           push(@{ $form->{TEMPLATE_ARRAYS}->{description} }, $form->format_amount($myconfig, $ref->{qty} * $form->{"qty_$i"}) . qq|, $ref->{partnumber}, $ref->{description}|);
1646           map({ push(@{ $form->{TEMPLATE_ARRAYS}->{$_} }, "") } grep({ $_ ne "description" } (@arrays, @prepared_arrays)));
1647         }
1648         $sth->finish;
1649       }
1650
1651       CVar->get_non_editable_ic_cvars(form               => $form,
1652                                       dbh                => $dbh,
1653                                       row                => $i,
1654                                       sub_module         => 'orderitems',
1655                                       may_converted_from => ['orderitems', 'invoice']);
1656
1657       push @{ $form->{TEMPLATE_ARRAYS}->{"ic_cvar_$_->{name}"} },
1658         CVar->format_to_template(CVar->parse($form->{"ic_cvar_$_->{name}_$i"}, $_), $_)
1659           for @{ $ic_cvar_configs };
1660
1661       push @{ $form->{TEMPLATE_ARRAYS}->{"project_cvar_" . $_->config->name} }, $_->value_as_text for @{ $project->cvars_by_config };
1662     }
1663   }
1664
1665   $form->{totalweight}       = $form->format_amount($myconfig, $totalweight, 3);
1666   $form->{totalweight_nofmt} = $totalweight;
1667   my $defaults = AM->get_defaults();
1668   $form->{weightunit}        = $defaults->{weightunit};
1669
1670   my $tax = 0;
1671   foreach $item (sort keys %taxaccounts) {
1672     $tax += $taxamount = $form->round_amount($taxaccounts{$item}, 2);
1673
1674     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase} },        $form->format_amount($myconfig, $taxbase{$item}, 2));
1675     push(@{ $form->{TEMPLATE_ARRAYS}->{taxbase_nofmt} },  $taxbase{$item});
1676     push(@{ $form->{TEMPLATE_ARRAYS}->{tax} },            $form->format_amount($myconfig, $taxamount,      2));
1677     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_nofmt} },      $taxamount);
1678     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate} },        $form->format_amount($myconfig, $form->{"${item}_rate"} * 100));
1679     push(@{ $form->{TEMPLATE_ARRAYS}->{taxrate_nofmt} },  $form->{"${item}_rate"} * 100);
1680     push(@{ $form->{TEMPLATE_ARRAYS}->{taxnumber} },      $form->{"${item}_taxnumber"});
1681     push(@{ $form->{TEMPLATE_ARRAYS}->{tax_id} },         $form->{"${item}_tax_id"});
1682
1683     if ( $form->{"${item}_tax_id"} ) {
1684       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"};
1685       my $description = $tax_obj ? $tax_obj->translated_attribute('taxdescription',  $form->{language_id}, 0) : '';
1686       push(@{ $form->{TEMPLATE_ARRAYS}->{taxdescription} }, $description . q{ } . 100 * $form->{"${item}_rate"} . q{%});
1687     }
1688   }
1689
1690   $form->{nodiscount_subtotal} = $form->format_amount($myconfig, $form->{nodiscount_total}, 2);
1691   $form->{discount_total}      = $form->format_amount($myconfig, $form->{discount_total}, 2);
1692   $form->{nodiscount}          = $form->format_amount($myconfig, $nodiscount, 2);
1693   $form->{yesdiscount}         = $form->format_amount($myconfig, $form->{nodiscount_total} - $nodiscount, 2);
1694
1695   if($form->{taxincluded}) {
1696     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal} - $tax, 2);
1697     $form->{subtotal_nofmt} = $form->{ordtotal} - $tax;
1698   } else {
1699     $form->{subtotal}       = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1700     $form->{subtotal_nofmt} = $form->{ordtotal};
1701   }
1702
1703   my $grossamount = ($form->{taxincluded}) ? $form->{ordtotal} : $form->{ordtotal} + $tax;
1704   $form->{ordtotal} = $form->round_amount( $grossamount, 2, 1);
1705   $form->{rounding} = $form->round_amount(
1706     $form->{ordtotal} - $form->round_amount($grossamount, 2),
1707     2
1708   );
1709
1710   # format amounts
1711   $form->{rounding} = $form->format_amount($myconfig, $form->{rounding}, 2);
1712   $form->{quototal} = $form->{ordtotal} = $form->format_amount($myconfig, $form->{ordtotal}, 2);
1713
1714   $form->set_payment_options($myconfig, $form->{$form->{type} =~ /_quotation/ ? 'quodate' : 'orddate'}, $form->{type});
1715
1716   $form->{username} = $myconfig->{name};
1717
1718   $form->{department}    = SL::DB::Manager::Department->find_by(id => $form->{department_id})->description if $form->{department_id};
1719   $form->{delivery_term} = SL::DB::Manager::DeliveryTerm->find_by(id => $form->{delivery_term_id} || undef);
1720   $form->{delivery_term}->description_long($form->{delivery_term}->translated_attribute('description_long', $form->{language_id})) if $form->{delivery_term} && $form->{language_id};
1721
1722   $form->{order} = SL::DB::Manager::Order->find_by(id => $form->{id}) if $form->{id};
1723   $form->{$_} = $form->format_amount($myconfig, $form->{$_}, 2) for @separate_totals;
1724
1725   $main::lxdebug->leave_sub();
1726 }
1727
1728 1;
1729
1730 __END__
1731
1732 =head1 NAME
1733
1734 OE.pm - Order entry module
1735
1736 =head1 DESCRIPTION
1737
1738 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>.
1739
1740 =head1 FUNCTIONS
1741
1742 =over 4
1743
1744 =item retrieve_simple PARAMS
1745
1746 simple OE retrieval by id. does not look up customer, vendor, units or any other stuff. only oe and orderitems.
1747
1748   my $order = retrieve_simple(id => 2);
1749
1750   $order => {
1751     %_OE_CONTENT,
1752     orderitems => [
1753       %_ORDERITEM_ROW_1,
1754       %_ORDERITEM_ROW_2,
1755       ...
1756     ]
1757   }
1758
1759 =back
1760
1761 =cut