Payments & Deposits(Rounding Problem). Fixed.
[fa-stable.git] / includes / ui / items_cart.inc
1 <?php
2 /**********************************************************************
3     Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL, 
5         as published by the Free Software Foundation, either version 3 
6         of the License, or (at your option) any later version.
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
10     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12 include_once($path_to_root . "/includes/prefs/sysprefs.inc");
13 include_once($path_to_root . "/inventory/includes/inventory_db.inc");
14
15 class items_cart
16 {
17         var $trans_type;
18         var $line_items;
19         var $gl_items;
20
21         var     $order_id;
22
23         var $from_loc;
24         var $to_loc;
25         var $tran_date;
26         var $doc_date;
27         var $event_date;
28         var $transfer_type;
29         var $increase;
30         var $memo_;
31         var $branch_id;
32         var $reference;
33         var $original_amount;
34         var $currency;
35         var $rate;
36         var $source_ref;
37         var $vat_category;
38
39         var $tax_info;  // tax info for the GL transaction
40
41         var $fixed_asset;
42
43         function __construct($type, $trans_no=0)
44         {
45                 $this->trans_type = $type;
46                 $this->order_id = $trans_no;
47                 $this->clear_items();
48                 if (in_array($type, array(ST_LOCTRANSFER, ST_INVADJUST, ST_COSTUPDATE, ST_MANUISSUE, ST_MANURECEIVE, ST_JOURNAL)))
49                         $this->currency = get_company_pref('curr_default');
50                 $this->rate = 1;
51         }
52
53         // --------------- line item functions
54
55         function add_to_cart($line_no, $stock_id, $qty, $standard_cost, $description=null)
56         {
57
58                 if (isset($stock_id) && $stock_id != "" && isset($qty))
59                 {
60                         $this->line_items[$line_no] = new line_item($stock_id, $qty,
61                                 $standard_cost, $description);
62                         return true;
63                 }
64                 else
65                 {
66                         // shouldn't come here under normal circumstances
67                         display_error("unexpected - adding an invalid item or null quantity", "", true);
68                 }
69
70                 return false;
71         }
72
73         function find_cart_item($stock_id)
74         {
75                 foreach($this->line_items as $line_no=>$line) {
76                         if ($line->stock_id == $stock_id)
77                                 return $this->line_items[$line_no];
78                 }
79                 return null;
80         }
81
82         function update_cart_item($line_no, $qty, $standard_cost)
83         {
84                 $this->line_items[$line_no]->quantity = $qty;
85                 $this->line_items[$line_no]->standard_cost = $standard_cost;
86         }
87
88         function remove_from_cart($line_no)
89         {
90                 array_splice($this->line_items, $line_no, 1);
91         }
92
93         function count_items()
94         {
95                 return count($this->line_items);
96         }
97
98         function check_qoh($location, $date_, $reverse=false)
99         {
100                 global $SysPrefs;
101
102                 $low_stock = array();
103
104         if (!$SysPrefs->allow_negative_stock())
105         {
106                         foreach ($this->line_items as $line_no => $line_item)
107                                 if (has_stock_holding($line_item->mb_flag) || is_fixed_asset($line_item->mb_flag))
108                                 {
109                                         $quantity = $line_item->quantity;
110                                         if ($reverse)
111                                                 $quantity = -$line_item->quantity;
112
113                                         if ($quantity >= 0)
114                                                 continue;
115
116                                         if (check_negative_stock($line_item->stock_id, $quantity, $location, $date_))
117                                                 $low_stock[] = $line_item->stock_id;
118                         }
119                 }
120                 return $low_stock;
121         }
122
123         // ----------- GL item functions
124
125         function add_gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null, $date=null)
126         {
127                 if (isset($code_id) && $code_id != "" && isset($amount) && isset($dimension_id)  &&
128                         isset($dimension2_id))
129                 {
130                         $this->gl_items[] = new gl_item($code_id, $dimension_id, $dimension2_id, $amount, $memo, $act_descr, $person_id, $date);
131                         return true;
132                 }
133                 else
134                 {
135                         // shouldn't come here under normal circumstances
136                         display_error("unexpected - invalid parameters in add_gl_item($code_id, $dimension_id, $dimension2_id, $amount,...)", "", true);
137                 }
138
139                 return false;
140         }
141
142         function update_gl_item($index, $code_id, $dimension_id, $dimension2_id, $amount, $memo='', $act_descr=null, $person_id=null)
143         {
144             $this->gl_items[$index]->code_id = $code_id;
145             $this->gl_items[$index]->person_id = $person_id;
146
147         $gl_type = is_subledger_account($code_id);
148         if ($person_id != null && $gl_type)
149                 {
150                         $this->gl_items[$index]->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
151                         $data = get_subaccount_data($code_id, $person_id);
152                         $this->gl_items[$index]->person_name = $data['name'];
153                         $this->gl_items[$index]->branch_id = $data['id'];
154                 } else
155                 {
156                         $this->gl_items[$index]->person_type_id = $this->gl_items[$index]->person_name = '';
157                 }
158                 $this->gl_items[$index]->dimension_id = $dimension_id;
159                 $this->gl_items[$index]->dimension2_id = $dimension2_id;
160                 $this->gl_items[$index]->amount = $amount;
161                 $this->gl_items[$index]->reference = $memo;
162                 if ($act_descr == null)
163                         $this->gl_items[$index]->description = get_gl_account_name($code_id);
164                 else
165                         $this->gl_items[$index]->description = $act_descr;
166
167         }
168
169         function remove_gl_item($index)
170         {
171                 array_splice($this->gl_items, $index, 1);
172         }
173
174         function count_gl_items()
175         {
176                 return count($this->gl_items);
177         }
178
179         function gl_items_total()
180         {
181                 $total = 0;
182                 foreach ($this->gl_items as $gl_item)
183                         $total += $gl_item->amount;
184                 return $total;
185         }
186
187         function gl_items_total_debit()
188         {
189                 $total = 0;
190                 foreach ($this->gl_items as $gl_item)
191                 {
192                         if ($gl_item->amount > 0)
193                                 $total += $gl_item->amount;
194                 }
195                 return $total;
196         }
197
198         function gl_items_total_credit()
199         {
200                 $total = 0;
201                 foreach ($this->gl_items as $gl_item)
202                 {
203                         if ($gl_item->amount < 0)
204                                 $total += $gl_item->amount;
205                 }
206                 return $total;
207         }
208
209         // ------------ common functions
210
211         function clear_items()
212         {
213         unset($this->line_items);
214                 $this->line_items = array();
215
216         unset($this->gl_items);
217                 $this->gl_items = array();
218
219         }
220         //
221         //      Check if cart contains virtual subaccount (AP/AR) postings
222         //
223         function has_sub_accounts()
224         {
225                 foreach ($this->gl_items as $gl_item)
226                 {
227                         if (is_subledger_account($gl_item->code_id))
228                                 return true;
229                 }
230                 return false;
231         }
232
233         //
234         //      Check if cart contains postings to tax accounts
235         //
236         function has_taxes()
237         {
238                 foreach ($this->gl_items as $gl_item)
239                 {
240                         if (is_tax_account($gl_item->code_id))
241                                 return true;
242                 }
243                 return false;
244         }
245
246         /*
247                 Collect tax info from the GL transaction lines and return as array of values:
248                         'tax_date'              - tax date
249                         'tax_group'             - related counterparty tax group
250                         'tax_category'  - tax category (not set for now)
251                         'net_amount' - tax amounts array indexed by tax type id
252                         'tax_in', 'tax_out' - tax amounts array indexed by tax type id
253                         'tax_reg' - tax register used
254         */
255         function collect_tax_info()
256         {
257                 $tax_info = array();
258                 $subledger_sum = $net_sum = 0;
259
260                 $tax_info['tax_date'] = $this->tran_date;
261                 $vat_percent = get_company_pref('partial_vat_percent');
262                 $factor = $vat_percent && ($this->vat_category == VC_PARTIAL) ? $vat_percent/100: 1;
263
264                 foreach($this->gl_items as $gl)
265                 {
266             if ($person_type = is_subledger_account($gl->code_id))
267                         {
268                                 $tax_info['person_type'] = $person_type < 0 ? PT_SUPPLIER : PT_CUSTOMER;
269                                 $tax_info['person_id'] = $gl->person_id;
270
271                                 if ($tax_info['person_type'] == PT_CUSTOMER)
272                                 {
273                                         $branch = get_default_branch($gl->person_id);
274                                         $tax_info['tax_group'] = $branch['tax_group_id'];
275                                 } else {
276                                         $supplier = get_supplier($gl->person_id);
277                                         $tax_info['tax_group'] = $supplier['tax_group_id'];
278                                 }
279                                 $subledger_sum += $gl->amount;
280                         } elseif ($tax_id = is_tax_account($gl->code_id))
281                         {
282                                 $tax_type = get_tax_type($tax_id);
283                                 if ($gl->code_id == $tax_type['purchasing_gl_code']) {
284                                         if (!isset($tax_info['tax_in'][$tax_id]))
285                                                 $tax_info['tax_in'][$tax_id] = 0;
286                                         $tax_info['tax_in'][$tax_id] += $gl->amount;
287                                         $tax_info['tax_reg'] = TR_INPUT;
288                                 } else {
289                                         if (!isset($tax_info['tax_out'][$tax_id]))
290                                                 $tax_info['tax_out'][$tax_id] = 0;
291                                         $tax_info['tax_out'][$tax_id] -= $gl->amount;
292                                         if (!isset($tax_info['tax_reg'])) // TR_INPUT has priority (EU are posted on both accounts)
293                                                 $tax_info['tax_reg'] = TR_OUTPUT;
294                                 }
295                                 if ($tax_type['rate'])
296                                 {
297                                         // assume transaction adjustment for negative tax in/out
298                                         $sign = (@$tax_info['tax_in'][$tax_id] < 0 || @$tax_info['tax_out'][$tax_id] < 0) ? -1 : 1;
299                                         // we can have both input and output tax postings in some cases like intra-EU trade.
300                                         // so just calculate net_amount from the higher in/out tax
301                                         $tax_info['net_amount'][$tax_id]
302                                                 = $sign*round2(max(abs(@$tax_info['tax_in'][$tax_id]), abs(@$tax_info['tax_out'][$tax_id]))/$tax_type['rate']*100, user_price_dec())/$factor;
303
304                                 }
305                         } else
306                                 $net_sum += $gl->amount;
307                 }
308                 // if no tax amount posted guess register type from person_type used (e.g. export invoice)
309                 if (!isset($tax_info['tax_reg']) && isset($tax_info['person_type']))
310                         $tax_info['tax_reg'] = $tax_info['person_type']==PT_CUSTOMER ? TR_OUTPUT : TR_INPUT;
311
312                 if (count_array(@$tax_info['net_amount']))      // guess exempt sales/purchase if any tax has been found
313                 {
314                         $ex_net = abs($net_sum) - @array_sum($tax_info['net_amount']);
315                         if ($ex_net != 0)
316                                 $tax_info['net_amount_ex'] = $ex_net;
317                 }
318
319                 return $tax_info;
320         }
321
322         function set_currency($curr, $rate=0)
323         {
324                 $this->currency = $curr;
325                 $this->rate = $rate;
326         }
327
328         /*
329                 Reduce number of necessary gl posting lines.
330         */
331         function reduce_gl()
332         {
333                 /* reduce additional postings */
334                 $codes = array();
335                 foreach($this->gl_items as $n => $gl)
336                 {
337                         $prev = @$codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference];
338                         if (isset($prev)) { // add amount to previous line for the same gl_code dims and memo
339                                 $this->gl_items[$prev]->amount += $gl->amount;
340                                 if ($this->gl_items[$prev]->amount == 0) // discard if overall amount==0
341                                 {
342                                         unset($this->gl_items[$prev], $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference]);
343                                 }
344                                 unset($this->gl_items[$n]);
345                         } else
346                                 $codes[$gl->code_id][$gl->person_id][$gl->dimension_id][$gl->dimension2_id][$gl->reference] = $n;
347                 }
348         }
349         /*
350                 Write transaction GL postings, creating tax records and updating AP/AR/bank ledger if needed.
351         */
352         function write_gl($check_balance = true)
353         {
354                 $delta = $this->gl_items_total();
355                 if ($check_balance && floatcmp($delta, 0) !=0)
356                 {
357                         $this->add_gl_item(get_company_pref($delta>0 ? 'rounding_db_act' : 'rounding_cr_act'),
358                                 0, 0, -$delta, '');
359                         error_log(sprintf( _("Rounding error %s encountered for trans_type:%s,trans_no:%s"), $delta, $this->trans_type, $this->order_id));
360                 }
361
362                 $bank_trans = $supp_trans = $cust_trans = array();
363                 $total_gl = 0;
364                 foreach($this->gl_items as $gl)
365                 {
366                         if (!isset($gl->date))
367                                 $gl->date = $this->tran_date;
368
369                         $total_gl += add_gl_trans($this->trans_type, $this->order_id, $gl->date, $gl->code_id, $gl->dimension_id, $gl->dimension2_id, 
370                                 $gl->reference, $gl->amount, $this->currency, $gl->person_type_id, $gl->person_id, "", $this->rate);
371
372                         // post to first found bank account using given gl acount code.
373                         $is_bank_to = is_bank_account($gl->code_id);
374                 if ($is_bank_to && (get_bank_account_currency($is_bank_to) == $this->currency)) // do not register exchange variations in bank trans
375                 {
376                         if (!isset($bank_trans[$is_bank_to]))
377                                 $bank_trans[$is_bank_to] = 0;
378
379                         $bank_trans[$is_bank_to] += $gl->amount;
380                 } elseif ($gl->person_id)
381                 {
382                         $home_currency = get_company_currency();
383                                 // collect per counterparty amounts (in case more than one posting was done to the account),
384                                 // do not post exchange variations to AR/AP (journal in not customer/supplier currency)
385                         if ($gl->person_type_id==PT_SUPPLIER && (get_supplier_currency($gl->person_id) == $this->currency || $this->currency != $home_currency))
386                                         $supp_trans[$gl->person_id] = @$supp_trans[$gl->person_id] + $gl->amount;
387                         elseif ($gl->person_type_id==PT_CUSTOMER && (get_customer_currency(null, $gl->branch_id) == $this->currency || $this->currency != $home_currency))
388                                         $cust_trans[$gl->branch_id] = @$cust_trans[$gl->branch_id] + $gl->amount;
389                 }
390
391                 }
392                 // post currency roundings if any
393                 if ($check_balance && floatcmp($total_gl, 0))
394                         add_gl_trans($this->trans_type, $this->order_id, $this->tran_date, 
395                                 get_company_pref($total_gl>0 ? 'rounding_db_act' : 'rounding_cr_act'), 0, 0, _('Exchange rate roundings'), -$total_gl);
396
397                 // update bank ledger if used
398                 foreach($bank_trans as $bank_id => $amount)
399                         add_bank_trans($this->trans_type, $this->order_id, $bank_id, $this->reference,
400                                 $this->tran_date, $amount, 0, "", $this->currency,
401                                 "Cannot insert a destination bank transaction");
402
403                 // add AP/AR for journal transaction
404                 if ($this->trans_type == ST_JOURNAL)
405                 {
406                         // update AR
407                         foreach($cust_trans as $branch_id => $amount)
408                                 if (floatcmp($amount, 0))
409                                         write_cust_journal($this->trans_type, $this->order_id, $branch_id, $this->tran_date,
410                                                 $this->reference, $amount, $this->rate);
411                         // update AP
412                         foreach($supp_trans as $supp_id => $amount)
413                                 if (floatcmp($amount, 0))
414                                         write_supp_journal($this->trans_type, $this->order_id, $supp_id, $this->tran_date,
415                                                 $this->reference, -$amount, $this->rate, $this->source_ref);
416                 }
417
418                 // generate tax records for journal transaction
419                 if ($this->trans_type == ST_JOURNAL && is_array($this->tax_info))
420                 {
421                         foreach($this->tax_info['net_amount'] as $tax_id => $net)
422                         {
423                                 if (!$net)
424                                         continue;
425
426                                 // in EU VAT system intra-community goods aquisition is posted to both purchasing and sales tax accounts,
427                                 // but included only in purchase register. To avoid double registering ELSE is used below!
428                                 if (isset($this->tax_info['tax_in'][$tax_id]))
429                                 {
430                                         $tax = $this->tax_info['tax_in'][$tax_id];
431                                         $reg = TR_INPUT;
432                                 }
433                                 elseif (isset($this->tax_info['tax_out'][$tax_id]))
434                                 {
435                                         $tax = $this->tax_info['tax_out'][$tax_id];
436                                         $reg = TR_OUTPUT;
437                                 }
438                                 elseif (isset($this->tax_info['tax_reg'])) // e.g. export
439                                 {
440                                         $tax = 0;
441                                         $reg = $this->tax_info['tax_reg'];
442                                 } else
443                                         continue;
444
445                                 $tax_nominal = $this->tax_info['rate'][$tax_id]/100*$net;
446                                 add_trans_tax_details($this->trans_type, $this->order_id,
447                                         $tax_id, $this->tax_info['rate'][$tax_id], 0, $tax_nominal, $net, $this->rate,
448                                         $this->tran_date,
449                                         $this->source_ref, $reg);
450                         }
451                 }
452         }
453 }
454
455 //--------------------------------------------------------------------------------------------
456
457 class line_item
458 {
459         var $stock_id;
460         var $item_description;
461         var $units;
462         var $mb_flag;
463
464         var $quantity;
465         var $price;
466         var $standard_cost;
467
468         function __construct($stock_id, $qty, $standard_cost=null, $description=null)
469         {
470                 $item_row = get_item($stock_id);
471
472                 if ($item_row == null)
473                         display_error("invalid item added to order : $stock_id", "");
474
475                 $this->mb_flag = $item_row["mb_flag"];
476                 $this->units = $item_row["units"];
477
478                 if ($description == null)
479                         $this->item_description = $item_row["description"];
480                 else
481                         $this->item_description = $description;
482
483                 if ($standard_cost == null)
484                         $this->standard_cost = $item_row["purchase_cost"];
485                 else
486                         $this->standard_cost = $standard_cost;
487
488                 $this->stock_id = $stock_id;
489                 $this->quantity = $qty;
490                 //$this->price = $price;
491                 $this->price = 0;
492         }
493 }
494
495 //---------------------------------------------------------------------------------------
496
497 class gl_item
498 {
499
500         var $code_id;
501         var $dimension_id;
502         var $dimension2_id;
503         var $amount;
504         var $reference;
505         var $description;
506         var $person_id;
507         var $person_type_id;
508         var $person_name;
509         var $branch_id;
510         var $date;
511
512         function __construct($code_id=null, $dimension_id=0, $dimension2_id=0, $amount=0, $memo='',
513                 $act_descr=null, $person_id=null, $date=null)
514         {
515                 //echo "adding $index, $code_id, $dimension_id, $amount, $reference<br>";
516
517                 if ($act_descr == null && $code_id)
518                         $this->description = get_gl_account_name($code_id);
519                 else
520                         $this->description = $act_descr;
521
522                 $this->code_id = $code_id;
523                 $this->person_id = $person_id;
524         $gl_type = is_subledger_account($code_id);
525         if ($person_id != null  && $gl_type)
526                 {
527                         $this->person_type_id = $gl_type > 0 ? PT_CUSTOMER : PT_SUPPLIER;
528                         $data = get_subaccount_data($code_id, $person_id);
529                         $this->person_name = $data['name'];
530                         $this->branch_id = $data['id'];
531                 }
532                 $this->dimension_id = $dimension_id;
533                 $this->dimension2_id = $dimension2_id;
534                 $this->amount = round2($amount, user_price_dec());
535                 $this->reference = $memo;
536                 $this->date = $date;
537         }
538 }