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