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