526dc1553cdb0cfa8b392e64228aef66d5781d7e
[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 __construct()
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                 $out .= $template;      // add postfix
155                 if (!preg_match('/^([^\{]*)?\{([^\}]*)\}(.*)/', $out, $match)) {        // parse index
156                         display_error(_("Missing numeric placeholder in refline definition."));
157                         return $out;
158                 }
159
160                 $prefix = $match[1];
161                 $postfix = $match[3];
162
163                 $db_info = get_systype_db_info($type);
164                 $trans_table = $db_info[0];
165                 $type_fld = $db_info[1];
166                 $tno_fld = $db_info[2];
167                 $ref_fld = $db_info[3];
168                 $type = db_escape($type);
169
170                 // retrieve last ref number in the refline from original transaction table
171                 $sql = "SELECT MAX(CAST(SUBSTR($ref_fld, ".(strlen($prefix)+1).",LENGTH($ref_fld)-".(strlen($postfix)+strlen($prefix)).") AS UNSIGNED))"
172                                 ." FROM `$trans_table` tbl
173                                         LEFT JOIN ".TB_PREF."voided v ON tbl.`$tno_fld`=v.id AND v.type=$type"
174                                 ." WHERE ISNULL(v.id)"
175                                 .($type_fld ? " AND tbl.`$type_fld`=$type" : '')
176                                 ." AND `$ref_fld` REGEXP ".db_escape('^'.preg_quote($prefix).'[0-9]*'.preg_quote($postfix).'$');
177                 $result = db_query($sql, 'cannot retrieve last reference');
178                 $result = db_fetch_row($result);
179
180                         // fill with zeros to the length of original index placeholder
181                 return $prefix.sprintf('%0'.strlen($match[2]).'d', $result[0]+1).$postfix;
182         }
183
184         //
185         //      Get/check transaction reference.
186         //      $ref!=null => check reference is not used (or unique for $trans_no!=0)
187         //  $trans!=0 $ref=null => retrieve reference for the $type/$trans_no (if any)
188         //
189         function _get($type, $trans_no=0, $ref=null)
190         {
191                 $db_info = get_systype_db_info($type);
192                 $trans_table = $db_info[0];
193                 $type_fld = $db_info[1];
194                 $tno_fld = $db_info[2];
195                 $ref_fld = $db_info[3];
196
197                 $type = db_escape($type);
198
199                 $sql = "SELECT `$ref_fld` 
200                                 FROM `$trans_table` tbl
201                                         LEFT JOIN ".TB_PREF."voided v ON 
202                                 tbl.`$tno_fld`=v.id AND v.type=$type"
203                         ." WHERE ISNULL(v.id)"
204                         .($type_fld ? " AND tbl.`$type_fld`=$type" : '');
205                 if ($ref)
206                 {
207                         $sql .= " AND tbl.`$ref_fld`=".db_escape(trim($ref));
208                         if ($trans_no)
209                                 $sql .= " AND tbl.`$tno_fld` != ".db_escape($trans_no);
210                 } else {
211                         $sql .= " AND tbl.`$tno_fld`=".db_escape($trans_no);
212                 }
213
214                 $result = db_query($sql, "could not test for unique reference");
215                 if (!$result)
216                         return false;
217
218                 $result = db_fetch_row($result);
219                 return $result[0];
220         }
221
222         function is_new_reference($ref, $type, $trans_no=0)
223         {
224                 return !$this->_get($type, $trans_no, $ref);
225         }
226
227         //
228         // Get default reference for new transaction.
229         //
230         function get_next($type, $line=null, $context=null) 
231         {
232
233                 if (isset($line))
234                         $refline = $this->reflines->get($line);
235                 else {
236                         $refs = $this->reflines->get_all("trans_type=".db_escape($type)." AND `default`");
237                         $refline = db_fetch($refs);
238                 }
239
240                 if ($this->_legacy_line($refline))
241                         return $refline['pattern'];
242
243                 return $this->_parse_next($type, $refline['prefix'].$refline['pattern'], $context);
244         }
245
246         /**
247         *       Normalize reference to format allowed by refline (optionally selected by prefix).
248         *       FIXME: currently this is fake function which return either input reference or 
249         *       next reference when no line has been recognized.
250         **/
251         function normalize($ref, $type, $context, $line=null)
252         {
253                 if (!isset($type)) // inquiries
254                         return $ref;
255
256                 if (!$line)
257                         $line = $this->reflines->find_refline_id($ref, $type);
258
259                 return $this->is_valid($ref, $type, $context, $line) ? $ref : $this->get_next($type, $line, $context);
260         }
261
262         //
263         // Check reference is valid before add/update transaction.
264         // FIXME: does not check leading zeros in number
265         //
266         function is_valid($reference, $type, $context=null, $line=null)
267         {
268                 if (!isset($line))
269                         $line = $this->reflines->find_refline_id($reference, $type, true);
270
271                 if (!isset($line))
272                         return false;
273
274                 $refline = $this->reflines->get($line);
275
276                 if ($this->_legacy_line($refline))      //legacy non-templated line
277                         return strlen(trim($reference)) > 0;
278
279                 $regex = preg_quote($refline['prefix'].$refline['pattern']);
280                 if (!is_array($context))
281                         $context = array('date'=>$context);
282
283                 $context['pos'] = $_SESSION["wa_current_user"]->pos;
284
285                 if (is_date(@$context['date']))
286                 {
287                         list($year4, $month, $day) = explode("-", date2sql($context['date']));
288                         $year2 = substr($year4, 2);
289                 } else
290                 {
291                         $month = '\d{2,}';
292                         $year2 = '\d{2,}';
293                         $year4 = '\d{4,}';
294                 }
295                 $cust = @$context['customer'] ? $context['customer'] : '\d+';
296                 $supp = @$context['supplier'] ? $context['supplier'] : '\d+';
297                 $branch = @$context['branch'] ? $context['branch'] : '\d+';
298                 $location = @$context['location'] ? $context['location'] : '[a-z0-9]+';
299                 $pos = @$context['pos'] ? $context['pos'] : '\d+';
300                 $user = sprintf("%02d", $_SESSION['wa_current_user']->user);
301
302                 $regex = preg_replace(
303                         array(
304                                 '/\\\{/',       // unquote placeholders
305                                 '/\\\}/',
306                                 '/\{MM\}/',
307                                 '/\{YY\}/',
308                                 '/\{YYYY\}/',
309                                 '/\{C\}/',
310                                 '/\{B\}/',
311                                 '/\{S\}/',
312                                 '/\{L\}/',
313                                 '/\{UU\}/',
314                                 '/\{P\}/',
315                                 '/\{\d+}/',
316                         ),
317                         array(
318                                 '{',
319                                 '}',
320                                 $month,
321                                 $year2,
322                                 $year4,
323                                 $cust,
324                                 $branch,
325                                 $supp,
326                                 $location,
327                                 $user,
328                                 $pos,
329                                 '\d+',
330                         ), $regex);
331
332                 $regex = '"^'.$regex.'"i';
333
334                 return preg_match($regex, $reference, $match) ? 1 : 0;
335         }
336
337         //
338         //      Save reference (and prepare next) on write transaction.
339         //
340         function save($type, $id, $reference, $line = null) 
341         {
342                 if ($reference == 'auto')
343                         return;
344
345             $sql = "REPLACE ".TB_PREF."refs SET reference=".db_escape($reference)
346                         .", type=".db_escape($type).", id=".db_escape($id);
347         db_query($sql, "could not update reference entry");
348
349                 if (!isset($line))
350                 {
351                         $line = $this->reflines->find_refline_id($reference, $type);
352                 }
353
354                 $refline = $this->reflines->get($line);
355                  // legacy code used with simple templates
356                 if ($this->_legacy_line($refline) && ($reference == $this->get_next($type, $line))) { // if reference was not changed from default
357                         $next = $this->_increment($reference);  // increment default
358                         $this->reflines->save_next($type, $next, $line);
359                 }
360         }
361         //
362         // Restore previous reference (if possible) after voiding transaction.
363         //
364         function restore_last($type, $id)
365         {
366                 // get refline for removed document reference
367                 $reference = get_reference($type, $id);
368                 $line = $this->reflines->find_refline_id($reference, $type);
369                 $refline = $this->reflines->get($line);
370
371                 if ($this->_legacy_line($refline)) // legacy code used with simple templates
372                 {
373                         $last = $this->_increment($this->get_next($type, $line), true); // find last reference used in this line
374                         if ($reference == $last)
375                         {
376                                 // save last reference as next
377                             $sql = "UPDATE ".TB_PREF."reflines SET pattern=SUBSTRING(" . db_escape(trim($last)) .", LENGTH(`prefix`)+1)"
378                                         . " WHERE trans_type = ".db_escape($type) . " AND `id`=".db_escape($line);
379
380                                 db_query($sql, "The next transaction ref for $type could not be updated");
381                         }
382                 }
383         }
384
385         //-----------------------------------------------------------------------
386         //
387         //      Increments (or decrements if $back==true) reference template
388         //
389         function _increment($reference, $back=false) 
390         {
391                 // Legacy code used when no palceholder is in use:
392                 //  WA036 will increment to WA037 and so on.
393         // If $reference contains at least one group of digits,
394         // extract first didgits group and add 1, then put all together.
395         // NB. preg_match returns 1 if the regex matches completely 
396         // also $result[0] holds entire string, 1 the first captured, 2 the 2nd etc.
397         //
398         if (preg_match('/^(\D*?)(\d+)(.*)/', $reference, $result) == 1) 
399         {
400                         list($all, $prefix, $number, $postfix) = $result;
401                         $dig_count = strlen($number); // How many digits? eg. 0003 = 4
402                         $fmt = '%0' . $dig_count . 'd'; // Make a format string - leading zeroes
403                         $val = intval($number + ($back ? ($number<1 ? 0 : -1) : 1));
404                         $nextval =  sprintf($fmt, $val); // Add one on, and put prefix back on
405
406                         return $prefix.$nextval.$postfix;
407         }
408         else 
409             return $reference;
410         }
411
412 }
413
414 //----------------------------------------------------------------------------
415 //
416 //      Check if reference was not used so far (for other transaction than $trans_no)
417 //
418 function is_new_reference($ref, $type, $trans_no=0)
419 {
420         global $Refs;
421
422         return $Refs->is_new_reference($ref, $type, $trans_no);
423 }
424
425 function get_reference($type, $trans_no)
426 {
427         global $Refs;
428
429         return $Refs->_get($type, $trans_no);
430 }