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