Prevent empty log lines and a couple of include cleanups.
[fa-stable.git] / includes / references.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/db/class.reflines_db.inc");
13 include_once($path_to_root . "/includes/types.inc");
14 //---------------------------------------------------------------------------------------------
15 // 2.4 - further changes toward removing refs table introduced:
16 //      . all transactions now have references stored in trans table.
17 //  . all reference related moved to class (is_new_reference yet preserved)
18 //      . template based reflines implemented
19 //
20 // FIXME:
21 //              - implement refline field in all transaction tables (obsoletes not always accurate find_refline_id)
22 //              - remove save() and restore_last() - for now preserved for reflines without placeholder
23 //              - see fixmes below
24 //              - remove refs table and create view instead (need e.g. CREATE VIEW support in db_import/db_export)
25
26 $refline_options = array(
27         ST_JOURNAL => array('date', 'user'),
28         ST_COSTUPDATE => array('date', 'user'),
29
30         ST_BANKPAYMENT => array('date', 'user'),
31         ST_BANKDEPOSIT => array('date', 'user'),
32         ST_BANKTRANSFER => array('date', 'user'),
33         ST_SUPPAYMENT => array('date', 'user'),
34         ST_CUSTPAYMENT => array('date', 'user'),
35
36         ST_SALESORDER => array('date', 'customer', 'branch', 'user', 'pos'),
37         ST_SALESQUOTE => array('date', 'customer', 'branch', 'user', 'pos'),
38         ST_SALESINVOICE => array('date', 'customer', 'branch', 'user', 'pos'),
39         ST_CUSTCREDIT => array('date', 'customer', 'branch', 'user', 'pos'),
40         ST_CUSTDELIVERY => array('date', 'customer', 'branch', 'user', 'pos'),
41
42         ST_LOCTRANSFER => array('date', 'location', 'user'),
43         ST_INVADJUST => array('date', 'location', 'user'),
44
45         ST_PURCHORDER => array('date', 'location', 'supplier', 'user'),
46         ST_SUPPINVOICE => array('date', 'location', 'supplier', 'user'),
47         ST_SUPPCREDIT => array('date', 'location', 'supplier', 'user'),
48         ST_SUPPRECEIVE => array('date', 'location', 'supplier', 'user'),
49
50         ST_WORKORDER => array('date', 'location', 'user'),
51         ST_MANUISSUE => array('date', 'location', 'user'),
52         ST_MANURECEIVE => array('date', 'user'),
53 );
54
55 $refline_placeholders = array(
56         'MM' => 'date',
57         'YY' => 'date',
58         'YYYY' => 'date',
59         'UU' => 'user',
60         'P' => 'pos',
61 //       FIXME:  for placeholders below all the code should work, but as the ref length is variable,
62 //         length specification in placeholder format should be implemented.
63 //      'C' => 'customer',
64 //      'B' => 'branch',
65 //      'S' => 'supplier',
66 //      'L' => 'location'
67 );
68
69 class references 
70 {
71         var $reflines;
72         
73         function references()
74         {
75                 $this->reflines = new reflines_db();
76         }
77
78         function _legacy_line($refline)
79         {
80                 return strpbrk($refline['pattern'], '{}') == false;
81         }
82
83         function _parse_next($type, $template, $context=null)
84         {
85                 global $refline_options, $refline_placeholders;
86
87                 // date based placeholders are always allowed, so default for non-array context is date
88                 if (!isset($context))
89                         $context = new_doc_date();
90
91                 if (is_string($context))
92                         $context = array('date' => $context);
93
94                 $context['user'] = $_SESSION['wa_current_user']->user;
95                 $context['pos'] = $_SESSION['wa_current_user']->pos;
96                 $out = '';
97
98                 while(($start = strpos($template, '{')) !==false) {
99
100                         $out .= substr($template, 0, $start);
101                         $stop = strpos($template, '}');
102                         if ($stop === false) {
103                                 display_warning(_("Invalid refline template."));
104                                 $out .= $template; // debug
105                                 break;
106                         }
107                         $ph = substr($template, $start+1, $stop-$start-1);
108                         $template = substr($template, $stop+1);
109
110                         if (isset($refline_placeholders[$ph])) {
111                                 if (!isset($context[$refline_placeholders[$ph]]))
112                                 {
113                                         display_warning(sprintf(_("Missing refline context data: '%s'"), $refline_placeholders[$ph]));
114                                         $out .= $ph; // debug
115                                 } else {
116                                         switch ($ph)
117                                         {
118                                                 case 'MM':
119                                                 case 'YY':
120                                                 case 'YYYY':
121                                                         list($day, $month, $year) = explode_date_to_dmy($context['date']);
122                                                         $out .= $ph == 'MM' ? sprintf('%02d', $month) : ($ph == 'YY' ? sprintf('%02d', $year%100): sprintf('%04d', $year));
123                                                         break;
124                                                 case 'C':
125                                                         $out .= sprintf('%d', $context['customer']);
126                                                         break;
127
128                                                 case 'B':
129                                                         $out .= sprintf('%d', $context['branch']);
130                                                         break;
131
132                                                 case 'S':
133                                                         $out .= sprintf('%d', $context['supplier']);
134                                                         break;
135
136                                                 case 'L':
137                                                         $out .= sprintf('%s', $context['location']);
138                                                         break;
139
140                                                 case 'P':
141                                                         $out .= sprintf('%s', $context['pos']);
142                                                         break;
143
144                                                 case 'UU':
145                                                         $out .= sprintf('%02d', $context['user']);
146                                                 break;
147                                         }
148                                 }
149                         } elseif (is_numeric($ph)) {
150                                 $out .= '{'.$ph.'}'; // index placeholder
151                         }
152                 }
153
154                 if (!preg_match('/^([^\{]*)?\{([^\}]*)\}(.*)/', $out, $match)) {        // parse index
155                         display_error(_("Missing numeric placeholder in refline definition."));
156                         return $out;
157                 }
158
159                 $prefix = $match[1];
160                 $postfix = $match[3];
161
162                 $db_info = get_systype_db_info($type);
163                 $trans_table = $db_info[0];
164                 $type_fld = $db_info[1];
165                 $tno_fld = $db_info[2];
166                 $ref_fld = $db_info[3];
167
168                 // retrieve last ref number in the refline from original transaction table
169                 $sql = "SELECT MAX(CAST(SUBSTR($ref_fld, ".(strlen($prefix)+1).",LENGTH($ref_fld)-".(strlen($postfix)+strlen($prefix)).") AS UNSIGNED))"
170                                 ." FROM `$trans_table` tbl
171                                         LEFT JOIN ".TB_PREF."voided v ON tbl.`$tno_fld`=v.id AND v.type=$type"
172                                 ." WHERE ISNULL(v.id)"
173                                 .($type_fld ? " AND tbl.`$type_fld`=$type" : '')
174                                 ." AND `$ref_fld` REGEXP ".db_escape('^'.preg_quote($prefix).'[0-9]*'.preg_quote($postfix).'$');
175                 $result = db_query($sql, 'cannot retrieve last reference');
176                 $result = db_fetch_row($result);
177
178                         // fill with zeros to the length of original index placeholder
179                 return $prefix.sprintf('%0'.strlen($match[2]).'d', $result[0]+1).$postfix;
180         }
181
182         //
183         //      Get/check transaction reference.
184         //      $ref!=null => check reference is not used (or unique for $trans_no!=0)
185         //  $trans!=0 $ref=null => retrieve reference for the $type/$trans_no (if any)
186         //
187         function _get($type, $trans_no=0, $ref=null)
188         {
189                 $db_info = get_systype_db_info($type);
190                 $trans_table = $db_info[0];
191                 $type_fld = $db_info[1];
192                 $tno_fld = $db_info[2];
193                 $ref_fld = $db_info[3];
194
195                 $type = db_escape($type);
196
197                 $sql = "SELECT `$ref_fld` 
198                                 FROM `$trans_table` tbl
199                                         LEFT JOIN ".TB_PREF."voided v ON 
200                                 tbl.`$tno_fld`=v.id AND v.type=$type"
201                         ." WHERE ISNULL(v.id)"
202                         .($type_fld ? " AND tbl.`$type_fld`=$type" : '');
203                 if ($ref)
204                 {
205                         $sql .= " AND tbl.`$ref_fld`=".db_escape(trim($ref));
206                         if ($trans_no)
207                                 $sql .= " AND tbl.`$tno_fld` != ".db_escape($trans_no);
208                 } else {
209                         $sql .= " AND tbl.`$tno_fld`=".db_escape($trans_no);
210                 }
211
212                 $result = db_query($sql, "could not test for unique reference");
213                 if (!$result)
214                         return false;
215
216                 $result = db_fetch_row($result);
217                 return $result[0];
218         }
219
220         function is_new_reference($ref, $type, $trans_no=0)
221         {
222                 return !$this->_get($type, $trans_no, $ref);
223         }
224
225         //
226         // Get default reference for new transaction.
227         //
228         function get_next($type, $line=null, $context=null) 
229         {
230
231                 if (isset($line))
232                         $refline = $this->reflines->get($line);
233                 else {
234                         $refs = $this->reflines->get_all("trans_type=".db_escape($type)." AND `default`");
235                         $refline = db_fetch($refs);
236                 }
237
238                 if ($this->_legacy_line($refline))
239                         return $refline['pattern'];
240
241                 return $this->_parse_next($type, $refline['prefix'].$refline['pattern'], $context);
242         }
243
244         /**
245         *       Normalize reference to format allowed by refline (optionally selected by prefix).
246         *       FIXME: currently this is fake function which return either input reference or 
247         *       next reference when no line has been recognized.
248         **/
249         function normalize($ref, $type, $context, $line=null)
250         {
251                 if (!isset($type)) // inquiries
252                         return $ref;
253
254                 if (!$line)
255                         $line = $this->reflines->find_refline_id($ref, $type);
256
257                 return $this->is_valid($ref, $type, $context, $line) ? $ref : $this->get_next($type, $line, $context);
258         }
259
260         //
261         // Check reference is valid before add/update transaction.
262         // FIXME: does not check leading zeros in number
263         //
264         function is_valid($reference, $type, $context=null, $line=null)
265         {
266                 if (!isset($line))
267                         $line = $this->reflines->find_refline_id($reference, $type, true);
268
269                 if (!isset($line))
270                         return false;
271
272                 $refline = $this->reflines->get($line);
273
274                 if ($this->_legacy_line($refline))      //legacy non-templated line
275                         return strlen(trim($reference)) > 0;
276
277                 $regex = preg_quote($refline['prefix'].$refline['pattern']);
278                 if (!is_array($context))
279                         $context = array('date'=>$context);
280
281                 $context['pos'] = $_SESSION["wa_current_user"]->pos;
282
283                 if (is_date(@$context['date']))
284                 {
285                         list($year4, $month, $day) = explode("-", date2sql($context['date']));
286                         $year2 = substr($year4, 2);
287                 } else
288                 {
289                         $month = '\d{2,}';
290                         $year2 = '\d{2,}';
291                         $year4 = '\d{4,}';
292                 }
293                 $cust = @$context['customer'] ? $context['customer'] : '\d+';
294                 $supp = @$context['supplier'] ? $context['supplier'] : '\d+';
295                 $branch = @$context['branch'] ? $context['branch'] : '\d+';
296                 $location = @$context['location'] ? $context['location'] : '[a-z0-9]+';
297                 $pos = @$context['pos'] ? $context['pos'] : '\d+';
298                 $user = sprintf("%02d", $_SESSION['wa_current_user']->user);
299
300                 $regex = preg_replace(
301                         array(
302                                 '/\\\{/',       // unquote placeholders
303                                 '/\\\}/',
304                                 '/\{MM\}/',
305                                 '/\{YY\}/',
306                                 '/\{YYYY\}/',
307                                 '/\{C\}/',
308                                 '/\{B\}/',
309                                 '/\{S\}/',
310                                 '/\{L\}/',
311                                 '/\{UU\}/',
312                                 '/\{P\}/',
313                                 '/\{\d+}/',
314                         ),
315                         array(
316                                 '{',
317                                 '}',
318                                 $month,
319                                 $year2,
320                                 $year4,
321                                 $cust,
322                                 $branch,
323                                 $supp,
324                                 $location,
325                                 $user,
326                                 $pos,
327                                 '\d+',
328                         ), $regex);
329
330                 $regex = '"^'.$regex.'"i';
331
332                 return preg_match($regex, $reference, $match) ? 1 : 0;
333         }
334
335         //
336         //      Save reference (and prepare next) on write transaction.
337         //
338         function save($type, $id, $reference, $line = null) 
339         {
340                 if ($reference == 'auto')
341                         return;
342
343             $sql = "REPLACE ".TB_PREF."refs SET reference=".db_escape($reference)
344                         .", type=".db_escape($type).", id=".db_escape($id);
345         db_query($sql, "could not update reference entry");
346
347                 if (!isset($line))
348                 {
349                         $line = $this->reflines->find_refline_id($reference, $type);
350                 }
351
352                 $refline = $this->reflines->get($line);
353                  // legacy code used with simple templates
354                 if ($this->_legacy_line($refline) && ($reference == $this->get_next($type, $line))) { // if reference was not changed from default
355                         $next = $this->_increment($reference);  // increment default
356                         $this->reflines->save_next($type, $next, $line);
357                 }
358         }
359         //
360         // Restore previous reference (if possible) after voiding transaction.
361         //
362         function restore_last($type, $id)
363         {
364                 // get refline for removed document reference
365                 $reference = get_reference($type, $id);
366                 $line = $this->reflines->find_refline_id($reference, $type);
367                 $refline = $this->reflines->get($line);
368
369                 if ($this->_legacy_line($refline)) // legacy code used with simple templates
370                 {
371                         $last = $this->_increment($this->get_next($type, $line), true); // find last reference used in this line
372                         if ($reference == $last)
373                         {
374                                 // save last reference as next
375                             $sql = "UPDATE ".TB_PREF."reflines SET pattern=SUBSTRING(" . db_escape(trim($last)) .", LENGTH(`prefix`)+1)"
376                                         . " WHERE trans_type = ".db_escape($type) . " AND `id`=".db_escape($line);
377
378                                 db_query($sql, "The next transaction ref for $type could not be updated");
379                         }
380                 }
381         }
382
383         //-----------------------------------------------------------------------
384         //
385         //      Increments (or decrements if $back==true) reference template
386         //
387         function _increment($reference, $back=false) 
388         {
389                 // Legacy code used when no palceholder is in use:
390                 //  WA036 will increment to WA037 and so on.
391         // If $reference contains at least one group of digits,
392         // extract first didgits group and add 1, then put all together.
393         // NB. preg_match returns 1 if the regex matches completely 
394         // also $result[0] holds entire string, 1 the first captured, 2 the 2nd etc.
395         //
396         if (preg_match('/^(\D*?)(\d+)(.*)/', $reference, $result) == 1) 
397         {
398                         list($all, $prefix, $number, $postfix) = $result;
399                         $dig_count = strlen($number); // How many digits? eg. 0003 = 4
400                         $fmt = '%0' . $dig_count . 'd'; // Make a format string - leading zeroes
401                         $val = intval($number + ($back ? ($number<1 ? 0 : -1) : 1));
402                         $nextval =  sprintf($fmt, $val); // Add one on, and put prefix back on
403
404                         return $prefix.$nextval.$postfix;
405         }
406         else 
407             return $reference;
408         }
409
410 }
411
412 //----------------------------------------------------------------------------
413 //
414 //      Check if reference was not used so far (for other transaction than $trans_no)
415 //
416 function is_new_reference($ref, $type, $trans_no=0)
417 {
418         global $Refs;
419
420         return $Refs->is_new_reference($ref, $type, $trans_no);
421 }
422
423 function get_reference($type, $trans_no)
424 {
425         global $Refs;
426
427         return $Refs->_get($type, $trans_no);
428 }