Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 529
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
zbsDAL_lineitems
0.00% covered (danger)
0.00%
0 / 529
0.00% covered (danger)
0.00%
0 / 11
32580
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 getLineitem
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
650
 getLineitems
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
930
 getLineItemCount
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 getLineitemsTaxSummary
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
420
 addUpdateLineitem
0.00% covered (danger)
0.00%
0 / 210
0.00% covered (danger)
0.00%
0 / 1
2756
 deleteLineitem
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
110
 deleteLineItemsForObject
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
182
 tidy_lineitem
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 recalculate
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
 db_ready_lineitem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * V3.0+
6 *
7 * Copyright 2020 Automattic
8 *
9 * Date: 14/01/19
10 */
11
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/*
15Note on these getting separate DAL OBJ LAYER (excerpt from WH DAL3.0 notes)
16    - There are a few tables which are nested (no separate DAL2.Obj file), for these reasons:
17        - Meta, Custom Fields, ObjLinks are used so universally that they have been left in main DAL2.php
18        - Event Reminders sit within Events.php obj layer file
19        - LineItems get their own file, while they could sit in DAL2, they're new, and using the object layer will speed up the code write (easy get funcs etc. using obj model). Arguably these + meta, custom fields, etc. could sit somewhere else (these are used for Quotes, Invs, Trans)
20*/
21
22/**
23 * ZBS DAL >> LineItems
24 *
25 * @author   Woody Hayday <hello@jetpackcrm.com>
26 * @version  2.0
27 * @access   public
28 * @see      https://jetpackcrm.com/kb
29 */
30class zbsDAL_lineitems extends zbsDAL_ObjectLayer {
31
32    protected $objectType     = ZBS_TYPE_LINEITEM;
33    protected $objectDBPrefix = 'zbsli_';
34    protected $objectModel    = array(
35
36        // ID
37        'ID'             => array(
38            'fieldname' => 'ID',
39            'format'    => 'int',
40        ),
41
42        // site + team generics
43        'zbs_site'       => array(
44            'fieldname' => 'zbs_site',
45            'format'    => 'int',
46        ),
47        'zbs_team'       => array(
48            'fieldname' => 'zbs_team',
49            'format'    => 'int',
50        ),
51        'zbs_owner'      => array(
52            'fieldname' => 'zbs_owner',
53            'format'    => 'int',
54        ),
55
56        // other fields
57        'order'          => array(
58            'fieldname' => 'zbsli_order',
59            'format'    => 'int',
60        ),
61        'title'          => array(
62            'fieldname' => 'zbsli_title',
63            'format'    => 'str',
64            'max_len'   => 300,
65        ),
66        'desc'           => array(
67            'fieldname' => 'zbsli_desc',
68            'format'    => 'str',
69            'max_len'   => 300,
70        ),
71        'quantity'       => array(
72            'fieldname' => 'zbsli_quantity',
73            'format'    => 'decimal',
74        ),
75        'price'          => array(
76            'fieldname' => 'zbsli_price',
77            'format'    => 'decimal',
78        ),
79        'currency'       => array(
80            'fieldname' => 'zbsli_currency',
81            'format'    => 'curr',
82        ),
83        'net'            => array(
84            'fieldname' => 'zbsli_net',
85            'format'    => 'decimal',
86        ),
87        'discount'       => array(
88            'fieldname' => 'zbsli_discount',
89            'format'    => 'decimal',
90        ),
91        'fee'            => array(
92            'fieldname' => 'zbsli_fee',
93            'format'    => 'decimal',
94        ),
95        'shipping'       => array(
96            'fieldname' => 'zbsli_shipping',
97            'format'    => 'decimal',
98        ),
99        'shipping_taxes' => array(
100            'fieldname' => 'zbsli_shipping_taxes',
101            'format'    => 'str',
102        ),
103        'shipping_tax'   => array(
104            'fieldname' => 'zbsli_shipping_tax',
105            'format'    => 'decimal',
106        ),
107        'taxes'          => array(
108            'fieldname' => 'zbsli_taxes',
109            'format'    => 'str',
110        ),
111        'tax'            => array(
112            'fieldname' => 'zbsli_tax',
113            'format'    => 'decimal',
114        ),
115        'total'          => array(
116            'fieldname' => 'zbsli_total',
117            'format'    => 'decimal',
118        ),
119        'created'        => array(
120            'fieldname' => 'zbsli_created',
121            'format'    => 'uts',
122        ),
123        'lastupdated'    => array(
124            'fieldname' => 'zbsli_lastupdated',
125            'format'    => 'uts',
126        ),
127
128    );
129
130    function __construct( $args = array() ) {
131
132        #} =========== LOAD ARGS ==============
133        $defaultArgs = array(
134
135            // 'tag' => false,
136
137        );
138        foreach ( $defaultArgs as $argK => $argV ) {
139            $this->$argK = $argV;
140            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
141                if ( is_array( $args[ $argK ] ) ) {
142                    $newData = $this->$argK;
143                    if ( ! is_array( $newData ) ) {
144                        $newData = array();
145                    } foreach ( $args[ $argK ] as $subK => $subV ) {
146                        $newData[ $subK ] = $subV;
147                    }$this->$argK = $newData;
148                } else {
149                    $this->$argK = $args[ $argK ]; }
150            }
151        }
152        #} =========== / LOAD ARGS =============
153    }
154
155    // ===============================================================================
156    // ===========   LINEITEM  =======================================================
157
158    /**
159     * returns full lineitem line +- details
160     *
161     * @param int id        lineitem id
162     * @param array                     $args   Associative array of arguments
163     *
164     * @return array lineitem object
165     */
166    public function getLineitem( $id = -1, $args = array() ) {
167
168        global $zbs;
169
170        #} =========== LOAD ARGS ==============
171        $defaultArgs = array(
172
173            // permissions
174            'ignoreowner' => false, // this'll let you not-check the owner of obj
175
176            // returns scalar ID of line
177            'onlyID'      => false,
178
179            'fields'      => false, // false = *, array = fieldnames
180
181        );
182        foreach ( $defaultArgs as $argK => $argV ) {
183            $$argK = $argV;
184            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
185                if ( is_array( $args[ $argK ] ) ) {
186                    $newData = $$argK;
187                    if ( ! is_array( $newData ) ) {
188                        $newData = array();
189                    } foreach ( $args[ $argK ] as $subK => $subV ) {
190                        $newData[ $subK ] = $subV;
191                    }$$argK = $newData;
192                } else {
193                    $$argK = $args[ $argK ]; }
194            }
195        }
196        #} =========== / LOAD ARGS =============
197
198        #} Check ID
199        $id = (int) $id;
200        if (
201            ( ! empty( $id ) && $id > 0 )
202            ||
203            ( ! empty( $email ) )
204            ||
205            ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) )
206            ) {
207
208            global $ZBSCRM_t, $wpdb;
209            $wheres          = array( 'direct' => array() );
210            $whereStr        = '';
211            $additionalWhere = '';
212            $params          = array();
213            $res             = array();
214            $extraSelect     = '';
215
216            #} ============= PRE-QUERY ============
217
218                $selector = 'lineitem.*';
219            if ( is_array( $fields ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
220                    $selector = '';
221
222                    // always needs id, so add if not present
223                if ( ! in_array( 'ID', $fields ) ) {
224                    $selector = 'lineitem.ID';
225                }
226
227                foreach ( $fields as $f ) {
228                    if ( ! empty( $selector ) ) {
229                        $selector .= ',';
230                    }
231                    $selector .= 'lineitem.' . $f;
232                }
233            } elseif ( $onlyID ) {
234                $selector = 'lineitem.ID';
235            }
236
237            #} ============ / PRE-QUERY ===========
238
239            #} Build query
240            $query = 'SELECT ' . $selector . $extraSelect . ' FROM ' . $ZBSCRM_t['lineitems'] . ' as lineitem';
241            #} ============= WHERE ================
242
243            if ( ! empty( $id ) && $id > 0 ) {
244
245                #} Add ID
246                $wheres['ID'] = array( 'ID', '=', '%d', $id );
247
248            }
249
250            #} ============ / WHERE ==============
251
252            #} Build out any WHERE clauses
253            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
254            $whereStr  = $wheresArr['where'];
255            $params    = $params + $wheresArr['params'];
256            #} / Build WHERE
257
258            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
259            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
260            $ownQ   = $this->ownershipSQL( $ignoreowner );
261            if ( ! empty( $ownQ ) ) {
262                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
263            }
264            #} / Ownership
265
266            #} Append to sql (this also automatically deals with sortby and paging)
267            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
268
269            try {
270
271                #} Prep & run query
272                $queryObj     = $this->prepare( $query, $params );
273                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
274
275            } catch ( Exception $e ) {
276
277                #} General SQL Err
278                $this->catchSQLError( $e );
279
280            }
281
282            #} Interpret Results (ROW)
283            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
284
285                #} Has results, tidy + return
286
287                    #} Only ID? return it directly
288                if ( $onlyID ) {
289                    return $potentialRes->ID;
290                }
291
292                    // tidy
293                if ( is_array( $fields ) ) {
294                    // guesses fields based on table col names
295                    $res = $this->lazyTidyGeneric( $potentialRes );
296                } else {
297                    // proper tidy
298                    $res = $this->tidy_lineitem( $potentialRes, $withCustomFields );
299                }
300
301                    return $res;
302
303            }
304        } // / if ID
305
306        return false;
307    }
308
309    /**
310     * returns lineitem detail lines
311     *
312     * @param array $args Associative array of arguments
313     *
314     * @return array of lineitem lines
315     */
316    public function getLineitems( $args = array() ) {
317
318        global $zbs;
319
320        #} ============ LOAD ARGS =============
321        $defaultArgs = array(
322
323            // Search/Filtering (leave as false to ignore)
324            'searchPhrase'      => '', // searches zbsli_title, zbsli_desc
325            'associatedObjType' => false, // e.g. ZBS_TYPE_QUOTE
326            'associatedObjID'   => false, // e.g. 123
327            // Note on associated types: They can be used:
328            // associatedObjType
329            // associatedObjType + associatedObjID
330            // BUT NOT JUST associatedObjID (would bring collisions)
331
332            'withCustomFields'  => false, // none yet anyhow
333
334            // returns
335            'count'             => false,
336
337            'sortByField'       => 'ID',
338            'sortOrder'         => 'ASC',
339            'page'              => 0, // this is what page it is (gets * by for limit)
340            'perPage'           => 100,
341            'whereCase'         => 'AND', // DEFAULT = AND
342
343            // permissions
344            'ignoreowner'       => false, // this'll let you not-check the owner of obj
345
346        );
347        foreach ( $defaultArgs as $argK => $argV ) {
348            $$argK = $argV;
349            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
350                if ( is_array( $args[ $argK ] ) ) {
351                    $newData = $$argK;
352                    if ( ! is_array( $newData ) ) {
353                        $newData = array();
354                    } foreach ( $args[ $argK ] as $subK => $subV ) {
355                        $newData[ $subK ] = $subV;
356                    }$$argK = $newData;
357                } else {
358                    $$argK = $args[ $argK ]; }
359            }
360        }
361        #} =========== / LOAD ARGS =============
362
363        global $ZBSCRM_t, $wpdb, $zbs;
364        $wheres          = array( 'direct' => array() );
365        $whereStr        = '';
366        $additionalWhere = '';
367        $params          = array();
368        $res             = array();
369        $joinQ           = '';
370        $extraSelect     = '';
371
372        #} ============= PRE-QUERY ============
373
374            #} Capitalise this
375            $sortOrder = strtoupper( $sortOrder );
376
377            #} If just count, turn off any extra gumpf
378            // if ($count) { }
379
380        #} ============ / PRE-QUERY ===========
381
382        #} Build query
383        $query = 'SELECT lineitem.*' . $extraSelect . ' FROM ' . $ZBSCRM_t['lineitems'] . ' as lineitem' . $joinQ;
384
385        #} Count override
386        if ( $count ) {
387            $query = 'SELECT COUNT(lineitem.ID) FROM ' . $ZBSCRM_t['lineitems'] . ' as lineitem' . $joinQ;
388        }
389
390        #} ============= WHERE ================
391
392            #} Add Search phrase
393        if ( ! empty( $searchPhrase ) ) {
394
395            // search? - ALL THESE COLS should probs have index of FULLTEXT in db?
396            $searchWheres                 = array();
397            $searchWheres['search_title'] = array( 'zbsli_title', 'LIKE', '%s', '%' . $searchPhrase . '%' );
398            $searchWheres['search_desc']  = array( 'zbsli_desc', 'LIKE', '%s', '%' . $searchPhrase . '%' );
399
400            // This generates a query like 'zbsli_fname LIKE %s OR zbsli_lname LIKE %s',
401            // which we then need to include as direct subquery (below) in main query :)
402            $searchQueryArr = $this->buildWheres( $searchWheres, '', array(), 'OR', false );
403
404            if ( is_array( $searchQueryArr ) && isset( $searchQueryArr['where'] ) && ! empty( $searchQueryArr['where'] ) ) {
405
406                // add it
407                $wheres['direct'][] = array( '(' . $searchQueryArr['where'] . ')', $searchQueryArr['params'] );
408
409            }
410        }
411
412            // associated object search
413            // simplifiers
414            $hasObjType = false;
415        $hasObjID       = false;
416        if ( ! empty( $associatedObjType ) && $associatedObjType > 0 ) {
417            $hasObjType = true;
418        }
419        if ( ! empty( $associatedObjID ) && $associatedObjID > 0 ) {
420            $hasObjID = true;
421        }
422
423            // switch depending on setup
424        if ( $hasObjType && $hasObjID ) {
425
426            // has id + type to match to (e.g. quote 123)
427            $wheres['associatedObjType'] = array( 'ID', 'IN', '(SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_LINEITEM . ' AND zbsol_objtype_to = %d AND zbsol_objid_to = %d)', array( $associatedObjType, $associatedObjID ) );
428
429        } elseif ( $hasObjType && ! $hasObjID ) {
430
431            // has type but no id
432            // e.g. line items attached to invoices
433            $wheres['associatedObjType'] = array( 'ID', 'IN', '(SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_LINEITEM . ' AND zbsol_objtype_to = %d)', $associatedObjType );
434
435        } elseif ( $hasObjID && ! $hasObjType ) {
436
437            // has id but no type
438            // DO NOTHING, this is dodgy to ever call :) as collision of objs
439        }
440
441        #} ============ / WHERE ===============
442
443        #} CHECK this + reset to default if faulty
444        if ( ! in_array( $whereCase, array( 'AND', 'OR' ) ) ) {
445            $whereCase = 'AND';
446        }
447
448        #} Build out any WHERE clauses
449        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params, $whereCase );
450        $whereStr  = $wheresArr['where'];
451        $params    = $params + $wheresArr['params'];
452        #} / Build WHERE
453
454        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
455        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
456        $ownQ   = $this->ownershipSQL( $ignoreowner, 'contact' );
457        if ( ! empty( $ownQ ) ) {
458            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
459        }
460        #} / Ownership
461
462        #} Append to sql (this also automatically deals with sortby and paging)
463        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
464
465        try {
466
467            #} Prep & run query
468            $queryObj = $this->prepare( $query, $params );
469
470            #} Catch count + return if requested
471            if ( $count ) {
472                return $wpdb->get_var( $queryObj );
473            }
474
475            #} else continue..
476            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
477
478        } catch ( Exception $e ) {
479
480            #} General SQL Err
481            $this->catchSQLError( $e );
482
483        }
484
485        #} Interpret results (Result Set - multi-row)
486        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
487
488            #} Has results, tidy + return
489            foreach ( $potentialRes as $resDataLine ) {
490
491                    // tidy
492                    $resArr = $this->tidy_lineitem( $resDataLine, $withCustomFields );
493
494                    $res[] = $resArr;
495
496            }
497        }
498
499        return $res;
500    }
501
502    /**
503     * Returns a count of lineitems (owned)
504     * .. inc by status
505     *
506     * @return int count
507     */
508    public function getLineItemCount( $args = array() ) {
509
510        #} ============ LOAD ARGS =============
511        $defaultArgs = array(
512
513            // Search/Filtering (leave as false to ignore)
514
515            // permissions
516            'ignoreowner' => true, // this'll let you not-check the owner of obj
517
518        );
519        foreach ( $defaultArgs as $argK => $argV ) {
520            $$argK = $argV;
521            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
522                if ( is_array( $args[ $argK ] ) ) {
523                    $newData = $$argK;
524                    if ( ! is_array( $newData ) ) {
525                        $newData = array();
526                    } foreach ( $args[ $argK ] as $subK => $subV ) {
527                        $newData[ $subK ] = $subV;
528                    }$$argK = $newData;
529                } else {
530                    $$argK = $args[ $argK ]; }
531            }
532        }
533        #} =========== / LOAD ARGS =============
534
535        $whereArr = array();
536
537        return $this->DAL()->getFieldByWHERE(
538            array(
539                'objtype'     => ZBS_TYPE_LINEITEM,
540                'colname'     => 'COUNT(ID)',
541                'where'       => $whereArr,
542                'ignoreowner' => $ignoreowner,
543            )
544        );
545    }
546
547    /**
548     * returns tax summary array, of taxes applicable to given lineitems
549     *
550     * @param array $args Associative array of arguments
551     *
552     * @return array summary of taxes (e.g. array(array('name' => 'VAT','rate' => 20, 'value' => 123.44)))
553     */
554    public function getLineitemsTaxSummary( $args = array() ) {
555
556        global $zbs;
557
558        #} ============ LOAD ARGS =============
559        $defaultArgs = array(
560
561            // pass lineitems objs in array
562            'lineItems' => array(),
563
564        );
565        foreach ( $defaultArgs as $argK => $argV ) {
566            $$argK = $argV;
567            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
568                if ( is_array( $args[ $argK ] ) ) {
569                    $newData = $$argK;
570                    if ( ! is_array( $newData ) ) {
571                        $newData = array();
572                    } foreach ( $args[ $argK ] as $subK => $subV ) {
573                        $newData[ $subK ] = $subV;
574                    }$$argK = $newData;
575                } else {
576                    $$argK = $args[ $argK ]; }
577            }
578        }
579        #} =========== / LOAD ARGS =============
580
581        $summaryData = array();
582
583        // calc
584        if ( is_array( $lineItems ) && count( $lineItems ) > 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable,WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
585
586            $lineItemTaxes = array();
587            $taxRateTable  = zeroBSCRM_taxRates_getTaxTableArr( true );
588
589            foreach ( $lineItems as $lineItem ) {
590
591                // got any taxes on ?
592                if ( isset( $lineItem['net'] ) && isset( $lineItem['taxes'] ) ) {
593
594                    $taxRatesToApply = array();
595
596                    // get any taxes...
597                    if ( strpos( $lineItem['taxes'], ',' ) ) {
598
599                        $taxRateIDs = explode( ',', $lineItem['taxes'] );
600                        if ( is_array( $taxRateIDs ) ) {
601                            $taxRatesToApply = $taxRateIDs;
602                        }
603                    } else {
604                        $taxRatesToApply[] = (int) $lineItem['taxes'];
605                    }
606
607                    // calc these ones + add to summary
608                    if ( is_array( $taxRatesToApply ) ) {
609                        foreach ( $taxRatesToApply as $taxRateID ) {
610
611                            $rateID = (int) $taxRateID;
612                            if ( isset( $taxRateTable[ $rateID ] ) ) {
613
614                                // get rate
615                                $rate = 0.0;
616                                if ( isset( $taxRateTable[ $rateID ]['rate'] ) ) {
617                                    $rate = (float) $taxRateTable[ $rateID ]['rate'];
618                                }
619
620                                // calc + add
621                                $itemNet = $lineItem['net'];
622                                if ( isset( $lineItem['discount'] ) ) {
623                                    $itemNet -= $lineItem['discount'];
624                                }
625                                $taxValue = round( $itemNet * ( $rate / 100 ), 2 );
626
627                                // add to summary
628                                if ( ! isset( $summaryData[ $rateID ] ) ) {
629
630                                    // new, add
631                                    $summaryData[ $rateID ] = array(
632
633                                        'name'  => $taxRateTable[ $rateID ]['name'],
634                                        'rate'  => $rate,
635                                        'value' => $taxValue,
636
637                                    );
638
639                                } else {
640
641                                    // +=
642                                    $summaryData[ $rateID ]['value'] += $taxValue;
643
644                                }
645                            } // else not set?
646
647                        } // / foreach tax rate to apply
648                    }
649                } // / if has net and taxes
650
651            } // / foreach line item
652
653        } // / if has items
654
655        return $summaryData;
656    }
657
658    /**
659     * adds or updates a lineitem object
660     *
661     * @param array $args Associative array of arguments
662     *              id (if update), owner, data (array of field data)
663     *
664     * @return int line ID
665     */
666    public function addUpdateLineitem( $args = array() ) {
667
668        global $ZBSCRM_t, $wpdb, $zbs;
669
670        #} ============ LOAD ARGS =============
671        $defaultArgs = array(
672
673            'id'                   => -1,
674            'owner'                => -1,
675
676            // assign to
677            'linkedObjType'        => -1,
678            'linkedObjID'          => -1,
679
680            // fields (directly)
681            'data'                 => array(
682
683                'order'          => '',
684                'title'          => '',
685                'desc'           => '',
686                'quantity'       => '',
687                'price'          => '',
688                'currency'       => '',
689                'net'            => '',
690                'discount'       => '',
691                'fee'            => '',
692                'shipping'       => '',
693                'shipping_taxes' => '',
694                'shipping_tax'   => '',
695                'taxes'          => '',
696                'tax'            => '',
697                'total'          => '',
698                'lastupdated'    => '',
699
700                // allow this to be set for sync etc.
701                'created'        => -1,
702
703            ),
704
705            'limitedFields'        => -1, // if this is set it OVERRIDES data (allowing you to set specific fields + leave rest in tact)
706            // ^^ will look like: array(array('key'=>x,'val'=>y,'type'=>'%s'))
707
708            'silentInsert'         => false, // this was for init Migration - it KILLS all IA for newLineitem (because is migrating, not creating new :) this was -1 before
709
710            'do_not_update_blanks' => false, // this allows you to not update fields if blank (same as fieldoverride for extsource -> in)
711
712            'calculate_totals'     => false, // This allows us to recalculate tax, subtotal, total via php (e.g. if added via api). Only works if not using limitedFields
713
714        );
715        foreach ( $defaultArgs as $argK => $argV ) {
716            $$argK = $argV;
717            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
718                if ( is_array( $args[ $argK ] ) ) {
719                    $newData = $$argK;
720                    if ( ! is_array( $newData ) ) {
721                        $newData = array();
722                    } foreach ( $args[ $argK ] as $subK => $subV ) {
723                        $newData[ $subK ] = $subV;
724                    }$$argK = $newData;
725                } else {
726                    $$argK = $args[ $argK ]; }
727            }
728        }
729
730        #} =========== / LOAD ARGS ============
731
732        #} ========== CHECK FIELDS ============
733
734            $id = (int) $id;
735
736            // here we check that the potential owner CAN even own
737        if ( $owner > 0 && ! user_can( $owner, 'admin_zerobs_usr' ) ) {
738            $owner = -1;
739        }
740
741            // if owner = -1, add current
742        if ( ! isset( $owner ) || $owner === -1 ) {
743            $owner = zeroBSCRM_user(); }
744
745        if ( is_array( $limitedFields ) ) {
746
747            // LIMITED UPDATE (only a few fields.)
748            if ( ! is_array( $limitedFields ) || count( $limitedFields ) <= 0 ) {
749                return false;
750            }
751            // REQ. ID too (can only update)
752            if ( empty( $id ) || $id <= 0 ) {
753                return false;
754            }
755        } else {
756
757            // NORMAL, FULL UPDATE
758
759                // (re)calculate the totals etc?
760            if ( $calculate_totals ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
761
762                        $data = $this->recalculate( $data );
763
764            }
765        }
766
767        #} ========= / CHECK FIELDS ===========
768
769        #} ========= OVERRIDE SETTING (Deny blank overrides) ===========
770
771            // either ext source + setting, or set by the func call
772        if ( $do_not_update_blanks ) {
773
774                // this setting says 'don't override filled-out data with blanks'
775                // so here we check through any passed blanks + convert to limitedFields
776                // only matters if $id is set (there is somt to update not add
777            if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
778
779                // get data to copy over (for now, this is required to remove 'fullname' etc.)
780                $dbData = $this->db_ready_lineitem( $data );
781                // unset($dbData['id']); // this is unset because we use $id, and is update, so not req. legacy issue
782                // unset($dbData['created']); // this is unset because this uses an obj which has been 'updated' against original details, where created is output in the WRONG format :)
783
784                $origData    = $data; // $data = array();
785                $limitedData = array(); // array(array('key'=>'zbsli_x','val'=>y,'type'=>'%s'))
786
787                // cycle through + translate into limitedFields (removing any blanks, or arrays (e.g. externalSources))
788                // we also have to remake a 'faux' data (removing blanks for tags etc.) for the post-update updates
789                foreach ( $dbData as $k => $v ) {
790
791                    $intV = (int) $v;
792
793                    // only add if valuenot empty
794                    if ( ! is_array( $v ) && ! empty( $v ) && $v != '' && $v !== 0 && $v !== -1 && $intV !== -1 ) {
795
796                        // add to update arr
797                        $limitedData[] = array(
798                            'key'  => 'zbsli_' . $k, // we have to add zbsli_ here because translating from data -> limited fields
799                            'val'  => $v,
800                            'type' => $this->getTypeStr( 'zbsli_' . $k ),
801                        );
802
803                        // add to remade $data for post-update updates
804                        $data[ $k ] = $v;
805
806                    }
807                }
808
809                // copy over
810                $limitedFields = $limitedData;
811
812            } // / if ID
813
814        } // / if do_not_update_blanks
815
816        #} ========= / OVERRIDE SETTING (Deny blank overrides) ===========
817
818        #} ========= BUILD DATA ===========
819
820            $update = false;
821        $dataArr    = array();
822        $typeArr    = array();
823
824        if ( is_array( $limitedFields ) ) {
825
826            // LIMITED FIELDS
827            $update = true;
828
829            // cycle through
830            foreach ( $limitedFields as $field ) {
831
832                // some weird case where getting empties, so added check
833                if ( ! empty( $field['key'] ) ) {
834                    $dataArr[ $field['key'] ] = $field['val'];
835                    $typeArr[]                = $field['type'];
836                }
837            }
838
839            // add update time
840            if ( ! isset( $dataArr['zbsli_lastupdated'] ) ) {
841                $dataArr['zbsli_lastupdated'] = time();
842                $typeArr[]                    = '%d'; }
843        } else {
844
845            // FULL UPDATE/INSERT
846
847                // UPDATE
848                $dataArr = array(
849
850                    // ownership
851                    // no need to update these (as of yet) - can't move teams etc.
852                    // 'zbs_site' => zeroBSCRM_installSite(),
853                    // 'zbs_team' => zeroBSCRM_installTeam(),
854                    // 'zbs_owner' => $owner,
855
856                    'zbsli_order'          => $data['order'],
857                    'zbsli_title'          => $data['title'],
858                    'zbsli_desc'           => $data['desc'],
859                    'zbsli_quantity'       => $data['quantity'],
860                    'zbsli_price'          => $data['price'],
861                    'zbsli_currency'       => $data['currency'],
862                    'zbsli_net'            => $data['net'],
863                    'zbsli_discount'       => $data['discount'],
864                    'zbsli_fee'            => $data['fee'],
865                    'zbsli_shipping'       => $data['shipping'],
866                    'zbsli_shipping_taxes' => $data['shipping_taxes'],
867                    'zbsli_shipping_tax'   => $data['shipping_tax'],
868                    'zbsli_taxes'          => $data['taxes'],
869                    'zbsli_tax'            => $data['tax'],
870                    'zbsli_total'          => $data['total'],
871                    'zbsli_lastupdated'    => time(),
872
873                );
874
875                $typeArr = array( // field data types
876                            // '%d',  // site
877                            // '%d',  // team
878                            // '%d',  // owner
879
880                    '%d',
881                    '%s',
882                    '%s',
883                    '%f',
884                    '%s',
885                    '%s',
886                    '%s',
887                    '%s',
888                    '%s',
889                    '%s',
890                    '%s',
891                    '%s',
892                    '%s',
893                    '%s',
894                    '%s',
895                    '%d',
896
897                );
898
899                if ( ! empty( $id ) && $id > 0 ) {
900
901                    // is update
902                    $update = true;
903
904                } else {
905
906                    // INSERT (get's few extra :D)
907                    $update               = false;
908                    $dataArr['zbs_site']  = zeroBSCRM_site();
909                    $typeArr[]            = '%d';
910                    $dataArr['zbs_team']  = zeroBSCRM_team();
911                    $typeArr[]            = '%d';
912                    $dataArr['zbs_owner'] = $owner;
913                    $typeArr[]            = '%d';
914                    if ( isset( $data['created'] ) && ! empty( $data['created'] ) && $data['created'] !== -1 ) {
915                        $dataArr['zbsli_created'] = $data['created'];
916                        $typeArr[]                = '%d';
917                    } else {
918                        $dataArr['zbsli_created'] = time();
919                        $typeArr[]                = '%d';
920                    }
921                }
922        }
923
924        #} ========= / BUILD DATA ===========
925
926        #} ============================================================
927        #} ========= CHECK force_uniques & not_empty & max_len ========
928
929            // if we're passing limitedFields we skip these, for now
930            // #v3.1 - would make sense to unique/nonempty check just the limited fields. #gh-145
931        if ( ! is_array( $limitedFields ) ) {
932
933            // verify uniques
934            if ( ! $this->verifyUniqueValues( $data, $id ) ) {
935                return false; // / fails unique field verify
936            }
937
938            // verify not_empty
939            if ( ! $this->verifyNonEmptyValues( $data ) ) {
940                return false; // / fails empty field verify
941            }
942        }
943
944            // whatever we do we check for max_len breaches and abbreviate to avoid wpdb rejections
945            $dataArr = $this->wpdbChecks( $dataArr );
946
947        #} ========= / CHECK force_uniques & not_empty ================
948        #} ============================================================
949
950        #} Check if ID present
951        if ( $update ) {
952
953                #} Attempt update
954            if ( $wpdb->update(
955                $ZBSCRM_t['lineitems'],
956                $dataArr,
957                array( // where
958                    'ID' => $id,
959                ),
960                $typeArr,
961                array( // where data types
962                    '%d',
963                )
964            ) !== false ) {
965
966                        // if passing limitedFields instead of data, we ignore the following
967                            // this doesn't work, because data is in args default as arr
968                            // if (isset($data) && is_array($data)){
969                            // so...
970                if ( ! isset( $limitedFields ) || ! is_array( $limitedFields ) || $limitedFields == -1 ) {
971
972                } // / if $data
973
974                        // linked to anything?
975                if ( $linkedObjType > 0 && $linkedObjID > 0 ) {
976
977                    // if not already got obj link, add it
978                    $c = $this->DAL()->getObjsLinksLinkedToObj(
979                        array(
980                            'objtypefrom' => ZBS_TYPE_LINEITEM, // line item type (10)
981                            'objtypeto'   => $linkedObjType, // obj type (e.g. inv)
982                            'objfromid'   => $id,
983                            'objtoid'     => $linkedObjID,
984                            'direction'   => 'both',
985                            'count'       => true,
986                            'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
987                        )
988                    );
989
990                    // add link (via append) if not present
991                    if ( $c <= 0 ) {
992                        $this->DAL()->addUpdateObjLink(
993                            array(
994                                'data' => array(
995                                    'objtypefrom' => ZBS_TYPE_LINEITEM,
996                                    'objtypeto'   => $linkedObjType,
997                                    'objfromid'   => $id,
998                                    'objtoid'     => $linkedObjID,
999                                    // not req. 'owner'         =>  $owner
1000                                ),
1001                            )
1002                        );
1003                    }
1004                }
1005
1006                        /*
1007                            Not necessary
1008                        #} INTERNAL AUTOMATOR
1009                        #} &
1010                        #} FALLBACKS
1011                        // UPDATING CONTACT
1012                        if (!$silentInsert){
1013
1014                            // IA General lineitem update (2.87+)
1015                            zeroBSCRM_FireInternalAutomator('lineitem.update',array(
1016                                'id'=>$id,
1017                                'againstid' => $id,
1018                                'data'=> $dataArr
1019                                ));
1020
1021
1022
1023                        } */
1024
1025                        // Successfully updated - Return id
1026                        return $id;
1027
1028            } else {
1029
1030                $msg = __( 'DB Update Failed', 'zero-bs-crm' );
1031                $zbs->DAL->addError( 302, $this->objectType, $msg, $dataArr );
1032
1033                // FAILED update
1034                return false;
1035
1036            }
1037        } else {
1038
1039            #} No ID - must be an INSERT
1040            if ( $wpdb->insert(
1041                $ZBSCRM_t['lineitems'],
1042                $dataArr,
1043                $typeArr
1044            ) > 0 ) {
1045
1046                    #} Successfully inserted, lets return new ID
1047                    $newID = $wpdb->insert_id;
1048
1049                    // linked to anything?
1050                if ( $linkedObjType > 0 && $linkedObjID > 0 ) {
1051
1052                    // if not already got obj link, add it
1053                    $c = $this->DAL()->getObjsLinksLinkedToObj(
1054                        array(
1055                            'objtypefrom' => ZBS_TYPE_LINEITEM, // line item type (10)
1056                            'objtypeto'   => $linkedObjType, // obj type (e.g. inv)
1057                            'objfromid'   => $newID,
1058                            'objtoid'     => $linkedObjID,
1059                            'direction'   => 'both',
1060                            'count'       => true,
1061                            'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
1062                        )
1063                    );
1064
1065                    // add link (via append) if not present
1066                    if ( $c <= 0 ) {
1067                        $this->DAL()->addUpdateObjLink(
1068                            array(
1069                                'data' => array(
1070                                    'objtypefrom' => ZBS_TYPE_LINEITEM,
1071                                    'objtypeto'   => $linkedObjType,
1072                                    'objfromid'   => $newID,
1073                                    'objtoid'     => $linkedObjID,
1074                                    // not req. 'owner'         =>  $owner
1075                                ),
1076                            )
1077                        );
1078                    }
1079                }
1080
1081                    /*
1082                    Not necessary
1083                    #} INTERNAL AUTOMATOR
1084                    #} &
1085                    #} FALLBACKS
1086                    // NEW CONTACT
1087                    if (!$silentInsert){
1088
1089                        #} Add to automator
1090                        zeroBSCRM_FireInternalAutomator('lineitem.new',array(
1091                            'id'=>$newID,
1092                            'data'=>$dataArr,
1093                            'extsource'=>$approvedExternalSource,
1094                            'automatorpassthrough'=>$automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
1095                            'extraMeta'=>$confirmedExtraMeta #} This is the "extraMeta" passed (as saved)
1096                        ));
1097
1098                    }
1099                    */
1100
1101                    return $newID;
1102
1103            } else {
1104
1105                $msg = __( 'DB Insert Failed', 'zero-bs-crm' );
1106                $zbs->DAL->addError( 303, $this->objectType, $msg, $dataArr );
1107
1108                #} Failed to Insert
1109                return false;
1110
1111            }
1112        }
1113
1114        return false;
1115    }
1116
1117    /**
1118     * deletes a lineitem object
1119     *
1120     * NOTE! Not to be used directly, or if so, manually delete the objlinks for this item<->obj (e.g. inv) else garbage kept in objlinks table
1121     * Use: deleteLineItemsForObject
1122     *
1123     * @param array $args Associative array of arguments
1124     *              id
1125     *
1126     * @return int success;
1127     */
1128    public function deleteLineitem( $args = array() ) {
1129
1130        global $ZBSCRM_t, $wpdb, $zbs;
1131
1132        #} ============ LOAD ARGS =============
1133        $defaultArgs = array(
1134
1135            'id'          => -1,
1136            'saveOrphans' => true,
1137
1138        );
1139        foreach ( $defaultArgs as $argK => $argV ) {
1140            $$argK = $argV;
1141            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1142                if ( is_array( $args[ $argK ] ) ) {
1143                    $newData = $$argK;
1144                    if ( ! is_array( $newData ) ) {
1145                        $newData = array();
1146                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1147                        $newData[ $subK ] = $subV;
1148                    }$$argK = $newData;
1149                } else {
1150                    $$argK = $args[ $argK ]; }
1151            }
1152        }
1153        #} =========== / LOAD ARGS ============
1154
1155        #} Check ID & Delete :)
1156        $id = (int) $id;
1157        if ( ! empty( $id ) && $id > 0 ) {
1158
1159            // delete orphans?
1160            if ( $saveOrphans === false ) {
1161
1162            }
1163
1164            return zeroBSCRM_db2_deleteGeneric( $id, 'lineitems' );
1165
1166        }
1167
1168        return false;
1169    }
1170
1171    /**
1172     * deletes all lineitem objects assigned to another obj (quote,inv,trans)
1173     *
1174     * @param array $args Associative array of arguments
1175     *              id
1176     *
1177     * @return int success;
1178     */
1179    public function deleteLineItemsForObject( $args = array() ) {
1180
1181        global $ZBSCRM_t, $wpdb, $zbs;
1182
1183        #} ============ LOAD ARGS =============
1184        $defaultArgs = array(
1185
1186            'objID'   => -1,
1187            'objType' => -1,
1188
1189        );
1190        foreach ( $defaultArgs as $argK => $argV ) {
1191            $$argK = $argV;
1192            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1193                if ( is_array( $args[ $argK ] ) ) {
1194                    $newData = $$argK;
1195                    if ( ! is_array( $newData ) ) {
1196                        $newData = array();
1197                    } foreach ( $args[ $argK ] as $subK => $subV ) {
1198                        $newData[ $subK ] = $subV;
1199                    }$$argK = $newData;
1200                } else {
1201                    $$argK = $args[ $argK ]; }
1202            }
1203        }
1204        #} =========== / LOAD ARGS ============
1205
1206        #} Check ID & Delete :)
1207        $objID = (int) $objID;
1208        if ( ! empty( $objID ) && $objID > 0 && ! empty( $objType ) && $objType > 0 ) {
1209
1210            $lineItems = $this->getLineitems(
1211                array(
1212                    'associatedObjType' => $objType,
1213                    'associatedObjID'   => $objID,
1214                    'perPage'           => 1000,
1215                    'ignoreowner'       => true,
1216                )
1217            );
1218
1219            $delcount = 0;
1220            if ( is_array( $lineItems ) ) {
1221                foreach ( $lineItems as $li ) {
1222
1223                    $delcount += $this->deleteLineitem( array( 'id' => $li['id'] ) );
1224
1225                    // also delete the objlink for this
1226                    $this->DAL()->deleteObjLinks(
1227                        array(
1228                            'objtypefrom' => ZBS_TYPE_LINEITEM,
1229                            'objtypeto'   => $objType,
1230                            'objfromid'   => $li['id'],
1231                            'objtoid'     => $objID,
1232                        )
1233                    );
1234
1235                }
1236            }
1237
1238            return $delcount;
1239
1240        }
1241
1242        return false;
1243    }
1244
1245    /**
1246     * tidy's the object from wp db into clean array
1247     *
1248     * @param array $obj (DB obj)
1249     *
1250     * @return array lineitem (clean obj)
1251     */
1252    private function tidy_lineitem( $obj = false, $withCustomFields = false ) {
1253
1254            $res = false;
1255
1256        if ( isset( $obj->ID ) ) {
1257            $res       = array();
1258            $res['id'] = $obj->ID;
1259            /*
1260            `zbs_site` INT NULL DEFAULT NULL,
1261            `zbs_team` INT NULL DEFAULT NULL,
1262            `zbs_owner` INT NOT NULL,
1263            */
1264            $res['owner'] = $obj->zbs_owner;
1265
1266            $res['order']            = (int) $obj->zbsli_order;
1267            $res['title']            = $this->stripSlashes( $obj->zbsli_title );
1268            $res['desc']             = $this->stripSlashes( $obj->zbsli_desc );
1269            $res['quantity']         = zeroBSCRM_format_quantity( $this->stripSlashes( $obj->zbsli_quantity ) );
1270            $res['price']            = $this->stripSlashes( $obj->zbsli_price );
1271            $res['currency']         = $this->stripSlashes( $obj->zbsli_currency );
1272            $res['net']              = $this->stripSlashes( $obj->zbsli_net );
1273            $res['discount']         = $this->stripSlashes( $obj->zbsli_discount );
1274            $res['fee']              = $this->stripSlashes( $obj->zbsli_fee );
1275            $res['shipping']         = $this->stripSlashes( $obj->zbsli_shipping );
1276            $res['shipping_taxes']   = $this->stripSlashes( $obj->zbsli_shipping_taxes );
1277            $res['shipping_tax']     = $this->stripSlashes( $obj->zbsli_shipping_tax );
1278            $res['taxes']            = $this->stripSlashes( $obj->zbsli_taxes );
1279            $res['tax']              = $this->stripSlashes( $obj->zbsli_tax );
1280            $res['total']            = $this->stripSlashes( $obj->zbsli_total );
1281            $res['created']          = (int) $obj->zbsli_created;
1282            $res['created_date']     = ( isset( $obj->zbsli_created ) && $obj->zbsli_created > 0 ) ? zeroBSCRM_locale_utsToDatetime( $obj->zbsli_created ) : false;
1283            $res['lastupdated']      = (int) $obj->zbsli_lastupdated;
1284            $res['lastupdated_date'] = ( isset( $obj->zbsli_lastupdated ) && $obj->zbsli_lastupdated > 0 ) ? zeroBSCRM_locale_utsToDatetime( $obj->zbsli_lastupdated ) : false;
1285
1286        }
1287
1288        return $res;
1289    }
1290
1291    /**
1292     * Takes whatever lineitem data available and re-calculates net, total, tax etc.
1293     * .. returning same obj with updated vals
1294     *
1295     * @param array $line_item The line item.
1296     *
1297     * @return array $lineItem
1298     */
1299    public function recalculate( $line_item = false ) {
1300
1301        if ( is_array( $line_item ) ) {
1302
1303            // subtotal (zbsi_net)
1304            // == line item Quantity * rate * tax%
1305            $total = 0.0;
1306
1307            if ( isset( $line_item ) && is_array( $line_item ) ) {
1308                // Subtotal
1309                if ( isset( $line_item['quantity'] ) && isset( $line_item['price'] ) ) {
1310
1311                    $quantity = (float) $line_item['quantity'];
1312                    $price    = (float) $line_item['price'];
1313
1314                    // Discount? (applied to gross)
1315                    // ALWAYS gross 0.00 value for lineitems (Where as at invoice level can be %)
1316                    $discount = 0;
1317                    if ( isset( $line_item['discount'] ) ) {
1318                        $discount = (float) $line_item['discount'];
1319                    }
1320
1321                    // gross
1322                    $sub_total_pre_discount = $quantity * $price;
1323
1324                    $sub_total = $sub_total_pre_discount - $discount;
1325
1326                    // tax - this should be logged against line item, but lets recalc
1327                    if ( isset( $line_item['taxes'] ) && $line_item['taxes'] !== -1 ) {
1328                        $line_item['tax'] = zeroBSCRM_taxRates_getTaxValue( $sub_total, $line_item['taxes'] );
1329                    }
1330
1331                    // total would have discount, shipping, but as above, not using per line item as at v3.0 mvp
1332                    $total = $sub_total + $line_item['tax'];
1333                }
1334            }
1335            $line_item['net']   = $sub_total_pre_discount;
1336            $line_item['total'] = $total;
1337
1338            return $line_item;
1339        }
1340        return false;
1341    }
1342
1343    /**
1344     * remove any non-db fields from the object
1345     * basically takes array like array('owner'=>1,'fname'=>'x','fullname'=>'x')
1346     * and returns array like array('owner'=>1,'fname'=>'x')
1347     * This does so based on the objectModel!
1348     *
1349     * @param array $obj (clean obj)
1350     *
1351     * @return array (db ready arr)
1352     */
1353    private function db_ready_lineitem( $obj = false ) {
1354
1355        // use the generic? (override here if necessary)
1356        return $this->db_ready_obj( $obj );
1357    }
1358
1359    // ===========  /   LINEITEM  =======================================================
1360    // ===============================================================================
1361} // / class