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