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