Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1143
0.00% covered (danger)
0.00%
0 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
zbsDAL_quotes
0.00% covered (danger)
0.00%
0 / 1143
0.00% covered (danger)
0.00%
0 / 26
121452
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 add_listview_filters
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getSingle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIDList
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getAll
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getFullCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getQuote
0.00% covered (danger)
0.00%
0 / 121
0.00% covered (danger)
0.00%
0 / 1
1560
 getQuotes
0.00% covered (danger)
0.00%
0 / 237
0.00% covered (danger)
0.00%
0 / 1
10100
 getQuoteCount
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 addUpdateQuote
0.00% covered (danger)
0.00%
0 / 384
0.00% covered (danger)
0.00%
0 / 1
11342
 addUpdateQuoteTags
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 setQuoteTemplate
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 addUpdateQuoteAccepted
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
110
 setQuoteStatus
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 deleteQuote
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
110
 tidy_quote
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
306
 getQuoteMeta
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteContent
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteTemplateID
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteAcceptedTime
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteAccepted
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteOwner
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getQuoteContactID
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getQuoteHash
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 db_ready_quote
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 listViewObj
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/*
4 * Jetpack CRM
5 * https://jetpackcrm.com
6 * V3.0+
7 *
8 * Copyright 2020 Automattic
9 *
10 * Date: 14/01/19
11 */
12
13/*
14======================================================
15    Breaking Checks ( stops direct access )
16    ====================================================== */
17    defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
18/*
19======================================================
20    / Breaking Checks
21    ====================================================== */
22
23/**
24 * ZBS DAL >> Quotes
25 *
26 * @author   Woody Hayday <hello@jetpackcrm.com>
27 * @version  2.0
28 * @access   public
29 * @see      https://jetpackcrm.com/kb
30 */
31class zbsDAL_quotes extends zbsDAL_ObjectLayer {
32
33    protected $objectType            = ZBS_TYPE_QUOTE;
34    protected $objectDBPrefix        = 'zbsq_';
35    protected $include_in_templating = true;
36    protected $objectModel           = array(
37        // ID
38        'ID'               => array(
39            'fieldname' => 'ID',
40            'format'    => 'int',
41        ),
42
43        // site + team generics
44        'zbs_site'         => array(
45            'fieldname' => 'zbs_site',
46            'format'    => 'int',
47        ),
48        'zbs_team'         => array(
49            'fieldname' => 'zbs_team',
50            'format'    => 'int',
51        ),
52        'zbs_owner'        => array(
53            'fieldname' => 'zbs_owner',
54            'format'    => 'int',
55        ),
56
57        // other fields
58        'id_override'      => array(
59            'fieldname' => 'zbsq_id_override',
60            'format'    => 'str',
61            'max_len'   => 128,
62        ),
63        'title'            => array(
64            // db model:
65            'fieldname'   => 'zbsq_title',
66            'format'      => 'str',
67            // output model
68            'input_type'  => 'text',
69            'label'       => 'Quote Title',
70            'placeholder' => 'e.g. New Website',
71            'essential'   => true,
72            'dal1key'     => 'name',
73            'max_len'     => 255,
74        ),
75        'currency'         => array(
76            'fieldname' => 'zbsq_currency',
77            'format'    => 'curr',
78            'max_len'   => 4,
79        ),
80        'value'            => array(
81            // db model:
82            'fieldname'   => 'zbsq_value',
83            'format'      => 'decimal',
84            // output model
85            'input_type'  => 'price',
86            'label'       => 'Quote Value',
87            'placeholder' => 'e.g. 500.00',
88            'essential'   => true,
89            'dal1key'     => 'val',
90        ),
91        'date'             => array(
92            // db model:
93            'fieldname'   => 'zbsq_date',
94            'format'      => 'uts',
95            'autoconvert' => 'date', // NOTE autoconvert makes buildObjArr autoconvert from a 'date' using localisation rules, to a GMT timestamp (UTS)
96            // output model
97            'input_type'  => 'date',
98            'label'       => 'Quote Date',
99            'placeholder' => '',
100            'essential'   => true,
101        ),
102        'template'         => array(
103            'fieldname' => 'zbsq_template',
104            'format'    => 'str',
105        ),
106        'content'          => array(
107            'fieldname' => 'zbsq_content',
108            'format'    => 'str',
109        ),
110        'notes'            => array(
111            // db model:
112            'fieldname'   => 'zbsq_notes',
113            'format'      => 'str',
114            // output model
115            'input_type'  => 'textarea',
116            'label'       => 'Notes',
117            'placeholder' => '',
118
119        ),
120        'send_attachments' => array(
121            'fieldname' => 'zbsq_send_attachments',
122            'format'    => 'bool',
123        ), // note, from 4.0.9 we removed this from the front-end ui as we now show a modal option pre-send allowing user to chose which pdf's to attach
124        'hash'             => array(
125            'fieldname' => 'zbsq_hash',
126            'format'    => 'str',
127        ),
128        'lastviewed'       => array(
129            'fieldname' => 'zbsq_lastviewed',
130            'format'    => 'uts',
131        ),
132        'viewed_count'     => array(
133            'fieldname' => 'zbsq_viewed_count',
134            'format'    => 'int',
135        ),
136        'accepted'         => array(
137            'fieldname' => 'zbsq_accepted',
138            'format'    => 'uts',
139        ),
140        'acceptedsigned'   => array(
141            'fieldname' => 'zbsq_acceptedsigned',
142            'format'    => 'str',
143            'max_len'   => 200,
144        ),
145        'acceptedip'       => array(
146            'fieldname' => 'zbsq_acceptedip',
147            'format'    => 'str',
148            'max_len'   => 64,
149        ),
150        'created'          => array(
151            'fieldname' => 'zbsq_created',
152            'format'    => 'uts',
153        ),
154        'lastupdated'      => array(
155            'fieldname' => 'zbsq_lastupdated',
156            'format'    => 'uts',
157        ),
158
159    );
160
161    // hardtyped list of types this object type is commonly linked to
162    protected $linkedToObjectTypes = array(
163
164        ZBS_TYPE_CONTACT,
165
166    );
167
168    function __construct( $args = array() ) {
169
170        #} =========== LOAD ARGS ==============
171        $defaultArgs = array(
172
173            // 'tag' => false,
174
175        );
176        foreach ( $defaultArgs as $argK => $argV ) {
177            $this->$argK = $argV;
178            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
179                if ( is_array( $args[ $argK ] ) ) {
180                    $newData = $this->$argK;
181                    if ( ! is_array( $newData ) ) {
182                        $newData = array();
183                    }
184                    foreach ( $args[ $argK ] as $subK => $subV ) {
185                        $newData[ $subK ] = $subV;
186                    }
187                    $this->$argK = $newData;
188                } else {
189                    $this->$argK = $args[ $argK ];
190                }
191            }
192        }
193        #} =========== / LOAD ARGS =============
194
195        add_filter( 'jpcrm_listview_filters', array( $this, 'add_listview_filters' ) );
196    }
197
198    /**
199     * Adds items to listview filter using `jpcrm_listview_filters` hook.
200     *
201     * @param array $listview_filters Listview filters.
202     */
203    public function add_listview_filters( $listview_filters ) {
204        global $zbs;
205        // Add statuses if enabled.
206        if ( $zbs->settings->get( 'filtersfromstatus' ) === 1 ) {
207            $statuses = array(
208                'draft'       => __( 'Draft', 'zero-bs-crm' ),
209                'accepted'    => __( 'Accepted', 'zero-bs-crm' ),
210                'notaccepted' => __( 'Not Accepted', 'zero-bs-crm' ),
211            );
212            foreach ( $statuses as $status_slug => $status_label ) {
213                $listview_filters[ ZBS_TYPE_QUOTE ]['status'][ 'status_' . $status_slug ] = $status_label;
214            }
215        }
216        return $listview_filters;
217    }
218
219    // ===============================================================================
220    // ===========   QUOTE  =======================================================
221
222    // generic get Company (by ID)
223    // Super simplistic wrapper used by edit page etc. (generically called via dal->contacts->getSingle etc.)
224    public function getSingle( $ID = -1 ) {
225
226        return $this->getQuote( $ID );
227    }
228
229    // generic get (by ID list)
230    // Super simplistic wrapper used by MVP Export v3.0
231    public function getIDList( $IDs = false ) {
232
233        return $this->getQuotes(
234            array(
235                'inArr'        => $IDs,
236                'withOwner'    => true,
237                'withAssigned' => true,
238                'page'         => -1,
239                'perPage'      => -1,
240            )
241        );
242    }
243
244    // generic get (EVERYTHING)
245    // expect heavy load!
246    public function getAll( $IDs = false ) {
247
248        return $this->getQuotes(
249            array(
250                'withOwner'    => true,
251                'withAssigned' => true,
252                'sortByField'  => 'ID',
253                'sortOrder'    => 'ASC',
254                'page'         => -1,
255                'perPage'      => -1,
256            )
257        );
258    }
259
260    // generic get count of (EVERYTHING)
261    public function getFullCount() {
262
263        return $this->getQuotes(
264            array(
265                'count'   => true,
266                'page'    => -1,
267                'perPage' => -1,
268            )
269        );
270    }
271
272    /**
273     * returns full quote line +- details
274     *
275     * @param int id        quote id
276     * @param array                  $args   Associative array of arguments
277     *
278     * @return array quote object
279     */
280    public function getQuote( $id = -1, $args = array() ) {
281
282        global $zbs;
283
284        #} =========== LOAD ARGS ==============
285        $defaultArgs = array(
286
287            // if these are passed, will search based on these
288            'id_override'       => false,
289            'externalSource'    => false,
290            'externalSourceUID' => false,
291            'hash'              => false,
292
293            // with what?
294            'withLineItems'     => true,
295            'withCustomFields'  => true,
296            'withAssigned'      => true, // return ['contact'] & ['company'] objs if has link
297            'withTags'          => false,
298            'withFiles'         => false,
299            'withOwner'         => false,
300
301            // permissions
302            'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_QUOTE ), // this'll let you not-check the owner of obj
303
304            // returns scalar ID of line
305            'onlyID'            => false,
306
307            'fields'            => false, // false = *, array = fieldnames
308
309        );
310        foreach ( $defaultArgs as $argK => $argV ) {
311            $$argK = $argV;
312            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
313                if ( is_array( $args[ $argK ] ) ) {
314                    $newData = $$argK;
315                    if ( ! is_array( $newData ) ) {
316                        $newData = array();
317                    }
318                    foreach ( $args[ $argK ] as $subK => $subV ) {
319                        $newData[ $subK ] = $subV;
320                    }
321                    $$argK = $newData;
322                } else {
323                    $$argK = $args[ $argK ];
324                }
325            }
326        }
327        #} =========== / LOAD ARGS =============
328
329        #} Check ID
330        $id = (int) $id;
331        if (
332            ( ! empty( $id ) && $id > 0 )
333            ||
334            ( ! empty( $email ) )
335            ||
336            ( ! empty( $hash ) )
337            ||
338            ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) )
339            ) {
340
341            global $ZBSCRM_t, $wpdb;
342            $wheres          = array( 'direct' => array() );
343            $whereStr        = '';
344            $additionalWhere = '';
345            $params          = array();
346            $res             = array();
347            $extraSelect     = '';
348
349            #} ============= PRE-QUERY ============
350
351            #} Custom Fields
352            if ( $withCustomFields && ! $onlyID ) {
353
354                #} Retrieve any cf
355                $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_QUOTE ) );
356
357                #} Cycle through + build into query
358                if ( is_array( $custFields ) ) {
359                    foreach ( $custFields as $cK => $cF ) {
360
361                        // add as subquery
362                        $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . " WHERE zbscf_objid = quote.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) '" . $cK . "'";
363
364                        // add params
365                        $params[] = $cK;
366                        $params[] = ZBS_TYPE_QUOTE;
367
368                    }
369                }
370            }
371
372            $selector = 'quote.*';
373            if ( is_array( $fields ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
374                $selector = '';
375
376                // always needs id, so add if not present
377                if ( ! in_array( 'ID', $fields ) ) {
378                    $selector = 'quote.ID';
379                }
380
381                foreach ( $fields as $f ) {
382                    if ( ! empty( $selector ) ) {
383                        $selector .= ',';
384                    }
385                    $selector .= 'quote.' . $f;
386                }
387            } elseif ( $onlyID ) {
388                $selector = 'quote.ID';
389            }
390
391            #} ============ / PRE-QUERY ===========
392
393            #} Build query
394            $query = 'SELECT ' . $selector . $extraSelect . ' FROM ' . $ZBSCRM_t['quotes'] . ' as quote';
395            #} ============= WHERE ================
396
397            if ( ! empty( $id ) && $id > 0 ) {
398
399                #} Add ID
400                $wheres['ID'] = array( 'ID', '=', '%d', $id );
401
402            }
403            if ( ! empty( $id_override ) && $id_override > 0 ) {
404
405                #} Add id_override
406                // $wheres['search_id'] = array('ID','LIKE','%s','%'.$id_override.'%');
407                $wheres['search_id2'] = array( 'zbsq_id_override', 'LIKE', '%s', '%' . $id_override . '%' );
408
409            }
410
411            if ( ! empty( $hash ) ) {
412
413                #} Add hash
414                $wheres['hash'] = array( 'zbsq_hash', '=', '%s', $hash );
415
416            }
417
418            if ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) ) {
419
420                $wheres['extsourcecheck'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_QUOTE . ' AND zbss_source = %s AND zbss_uid = %s)', array( $externalSource, $externalSourceUID ) );
421
422            }
423
424            #} ============ / WHERE ==============
425
426            #} Build out any WHERE clauses
427            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
428            $whereStr  = $wheresArr['where'];
429            $params    = $params + $wheresArr['params'];
430            #} / Build WHERE
431
432            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
433            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
434            $ownQ   = $this->ownershipSQL( $ignoreowner, 'quote' );
435            if ( ! empty( $ownQ ) ) {
436                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
437            }
438            #} / Ownership
439
440            #} Append to sql (this also automatically deals with sortby and paging)
441            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
442
443            try {
444
445                #} Prep & run query
446                $queryObj     = $this->prepare( $query, $params );
447                $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
448
449            } catch ( Exception $e ) {
450
451                #} General SQL Err
452                $this->catchSQLError( $e );
453
454            }
455
456            #} Interpret Results (ROW)
457            if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
458
459                #} Has results, tidy + return
460
461                #} Only ID? return it directly
462                if ( $onlyID ) {
463                    return $potentialRes->ID;
464                }
465
466                // tidy
467                if ( is_array( $fields ) ) {
468                    // guesses fields based on table col names
469                    $res = $this->lazyTidyGeneric( $potentialRes );
470                } else {
471                    // proper tidy
472                    $res = $this->tidy_quote( $potentialRes, $withCustomFields );
473                }
474
475                if ( $withLineItems ) {
476
477                    // add all line item lines
478                    $res['lineitems'] = $this->DAL()->lineitems->getLineitems(
479                        array(
480                            'associatedObjType' => ZBS_TYPE_QUOTE,
481                            'associatedObjID'   => $potentialRes->ID,
482                            'perPage'           => 1000,
483                            'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
484                        )
485                    );
486
487                }
488
489                if ( $withAssigned ) {
490
491                    /*
492                    This is for MULTIPLE (e.g. multi contact/companies assigned to an inv)
493
494                        // add all assigned contacts/companies
495                        $res['contacts'] = $this->DAL()->contacts->getContacts(array(
496                            'hasObjTypeLinkedTo'=>ZBS_TYPE_QUOTE,
497                            'hasObjIDLinkedTo'=>$resDataLine->ID,
498                            'perPage'=>-1,
499                            'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_CONTACT)));
500
501                        $res['companies'] = $this->DAL()->companies->getCompanies(array(
502                            'hasObjTypeLinkedTo'=>ZBS_TYPE_QUOTE,
503                            'hasObjIDLinkedTo'=>$resDataLine->ID,
504                            'perPage'=>-1,
505                            'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_COMPANY)));
506
507                    .. but we use 1:1, at least now: */
508
509                        // add all assigned contacts/companies
510                        $res['contact'] = $this->DAL()->contacts->getContacts(
511                            array(
512                                'hasObjTypeLinkedTo' => ZBS_TYPE_QUOTE,
513                                'hasObjIDLinkedTo'   => $potentialRes->ID,
514                                'page'               => 0,
515                                'perPage'            => 1, // FORCES 1,
516                                'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
517                            )
518                        );
519
520                        $res['company'] = $this->DAL()->companies->getCompanies(
521                            array(
522                                'hasObjTypeLinkedTo' => ZBS_TYPE_QUOTE,
523                                'hasObjIDLinkedTo'   => $potentialRes->ID,
524                                'page'               => 0,
525                                'perPage'            => 1, // FORCES 1,
526                                'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_COMPANY ),
527                            )
528                        );
529
530                }
531
532                if ( $withFiles ) {
533
534                    $res['files'] = zeroBSCRM_files_getFiles( 'quote', $potentialRes->ID );
535
536                }
537
538                if ( $withTags ) {
539
540                    // add all tags lines
541                    $res['tags'] = $this->DAL()->getTagsForObjID(
542                        array(
543                            'objtypeid' => ZBS_TYPE_QUOTE,
544                            'objid'     => $potentialRes->ID,
545                        )
546                    );
547
548                }
549
550                return $res;
551
552            }
553        } // / if ID
554
555        return false;
556    }
557
558    /**
559     * returns quote detail lines
560     *
561     * @param array $args Associative array of arguments
562     *
563     * @return array of quote lines
564     */
565    public function getQuotes( $args = array() ) {
566
567        global $zbs;
568
569        #} ============ LOAD ARGS =============
570        $defaultArgs = array(
571
572            // Search/Filtering (leave as false to ignore)
573            'searchPhrase'     => '', // searches id, id override, title, content
574            'inArr'            => false,
575            'isTagged'         => false, // 1x INT OR array(1,2,3)
576            'isNotTagged'      => false, // 1x INT OR array(1,2,3)
577            'ownedBy'          => false,
578            'externalSource'   => false, // e.g. paypal
579            'olderThan'        => false, // uts
580            'newerThan'        => false, // uts
581            'hasAccepted'      => false, // if TRUE only returns accepted
582            'hasNotAccepted'   => false, // if TRUE only returns not-accepted
583            'assignedContact'  => false, // assigned to contact id (int)
584            'assignedCompany'  => false, // assigned to company id (int)
585            'quickFilters'     => false, // booo
586
587            // returns
588            'count'            => false,
589            'withLineItems'    => true,
590            'withCustomFields' => true,
591            'withTags'         => false,
592            'withOwner'        => false,
593            'withAssigned'     => false, // return ['contact'] & ['company'] objs if has link
594            'withFiles'        => false,
595            'suppressContent'  => false, // do not return html
596            'onlyColumns'      => false, // if passed (array('fname','lname')) will return only those columns (overwrites some other 'return' options). NOTE: only works for base fields (not custom fields)
597
598            'sortByField'      => 'ID',
599            'sortOrder'        => 'ASC',
600            'page'             => 0, // this is what page it is (gets * by for limit)
601            'perPage'          => 100,
602            'whereCase'        => 'AND', // DEFAULT = AND
603
604            // permissions
605            'ignoreowner'      => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_QUOTE ), // this'll let you not-check the owner of obj
606
607        );
608        foreach ( $defaultArgs as $argK => $argV ) {
609            $$argK = $argV;
610            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
611                if ( is_array( $args[ $argK ] ) ) {
612                    $newData = $$argK;
613                    if ( ! is_array( $newData ) ) {
614                        $newData = array();
615                    }
616                    foreach ( $args[ $argK ] as $subK => $subV ) {
617                        $newData[ $subK ] = $subV;
618                    }
619                    $$argK = $newData;
620                } else {
621                    $$argK = $args[ $argK ];
622                }
623            }
624        }
625        #} =========== / LOAD ARGS =============
626
627        global $ZBSCRM_t, $wpdb, $zbs;
628        $wheres          = array( 'direct' => array() );
629        $whereStr        = '';
630        $additionalWhere = '';
631        $params          = array();
632        $res             = array();
633        $joinQ           = '';
634        $extraSelect     = '';
635
636        #} ============= PRE-QUERY ============
637
638        #} Capitalise this
639        $sortOrder = strtoupper( $sortOrder );
640
641        #} If just count, turn off any extra gumpf
642        if ( $count ) {
643            $withCustomFields = false;
644            $withTags         = false;
645            $withFiles        = false;
646            $withOwner        = false;
647            $withAssigned     = false;
648        }
649
650        #} If onlyColumns, validate
651        if ( $onlyColumns ) {
652
653            #} onlyColumns build out a field arr
654            if ( is_array( $onlyColumns ) && count( $onlyColumns ) > 0 ) {
655
656                $onlyColumnsFieldArr = array();
657                foreach ( $onlyColumns as $col ) {
658
659                    // find db col key from field key (e.g. fname => zbsc_fname)
660                    $dbCol = '';
661                    if ( isset( $this->objectModel[ $col ] ) && isset( $this->objectModel[ $col ]['fieldname'] ) ) {
662                        $dbCol = $this->objectModel[ $col ]['fieldname'];
663                    }
664
665                    if ( ! empty( $dbCol ) ) {
666
667                        $onlyColumnsFieldArr[ $dbCol ] = $col;
668
669                    }
670                }
671            }
672
673            // if legit cols:
674            if ( isset( $onlyColumnsFieldArr ) && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
675
676                $onlyColumns = true;
677
678                // If onlyColumns, turn off extras
679                $withCustomFields = false;
680                $withTags         = false;
681                $withFiles        = false;
682                $withOwner        = false;
683                $withAssigned     = false;
684
685            } else {
686
687                // deny
688                $onlyColumns = false;
689
690            }
691        }
692
693        #} Custom Fields
694        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
695        if ( $withCustomFields ) {
696
697            #} Retrieve any cf
698            $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_QUOTE ) );
699
700            #} Cycle through + build into query
701            if ( is_array( $custFields ) ) {
702                foreach ( $custFields as $cK => $cF ) {
703
704                    // custom field (e.g. 'third name') it'll be passed here as 'third-name'
705                    // ... problem is mysql does not like that :) so we have to chage here:
706                    // in this case we prepend cf's with cf_ and we switch - for _
707                    $cKey = 'cf_' . str_replace( '-', '_', $cK );
708
709                    // we also check the $sortByField in case that's the same cf
710                    if ( $cK == $sortByField ) {
711
712                        // sort by
713                        $sortByField = $cKey;
714
715                        // check if sort needs any CAST (e.g. numeric):
716                        $sortByField = $this->DAL()->build_custom_field_order_by_str( $sortByField, $cF );
717
718                    }
719
720                    // add as subquery
721                    $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = quote.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) ' . $cKey;
722
723                    // add params
724                    $params[] = $cK;
725                    $params[] = ZBS_TYPE_QUOTE;
726
727                }
728            }
729        }
730
731        #} ============ / PRE-QUERY ===========
732
733        #} Build query
734        $query = 'SELECT quote.*' . $extraSelect . ' FROM ' . $ZBSCRM_t['quotes'] . ' as quote' . $joinQ;
735
736        #} Count override
737        if ( $count ) {
738            $query = 'SELECT COUNT(quote.ID) FROM ' . $ZBSCRM_t['quotes'] . ' as quote' . $joinQ;
739        }
740
741        #} onlyColumns override
742        if ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
743
744            $columnStr = '';
745            foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
746
747                if ( ! empty( $columnStr ) ) {
748                    $columnStr .= ',';
749                }
750                // this presumes str is db-safe? could do with sanitation?
751                $columnStr .= $colDBKey;
752
753            }
754
755            $query = 'SELECT ' . $columnStr . ' FROM ' . $ZBSCRM_t['quotes'] . ' as quote' . $joinQ;
756
757        }
758
759        #} ============= WHERE ================
760
761        #} Add Search phrase
762        if ( ! empty( $searchPhrase ) ) {
763
764            // search? - ALL THESE COLS should probs have index of FULLTEXT in db?
765            $searchWheres                       = array();
766            $searchWheres['search_id']          = array( 'ID', 'LIKE', '%s', '%' . $searchPhrase . '%' );
767            $searchWheres['search_id_override'] = array( 'zbsq_id_override', 'LIKE', '%s', '%' . $searchPhrase . '%' );
768            $searchWheres['search_title']       = array( 'zbsq_title', 'LIKE', '%s', '%' . $searchPhrase . '%' );
769            $searchWheres['search_content']     = array( 'zbsq_content', 'LIKE', '%s', '%' . $searchPhrase . '%' );
770
771            // 3.0.13 - Added ability to search custom fields (optionally)
772            $customFieldSearch = zeroBSCRM_getSetting( 'customfieldsearch' );
773            if ( $customFieldSearch == 1 ) {
774
775                // simplistic add
776                // NOTE: This IGNORES ownership of custom field lines.
777                $searchWheres['search_customfields'] = array( 'ID', 'IN', '(SELECT zbscf_objid FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objval LIKE %s AND zbscf_objtype = ' . ZBS_TYPE_QUOTE . ')', '%' . $searchPhrase . '%' );
778
779            }
780
781            // This generates a query like 'zbsq_fname LIKE %s OR zbsq_lname LIKE %s',
782            // which we then need to include as direct subquery (below) in main query :)
783            $searchQueryArr = $this->buildWheres( $searchWheres, '', array(), 'OR', false );
784
785            if ( is_array( $searchQueryArr ) && isset( $searchQueryArr['where'] ) && ! empty( $searchQueryArr['where'] ) ) {
786
787                // add it
788                $wheres['direct'][] = array( '(' . $searchQueryArr['where'] . ')', $searchQueryArr['params'] );
789
790            }
791        }
792
793        #} In array (if inCompany passed, this'll currently overwrite that?! (todo2.5))
794        if ( is_array( $inArr ) && count( $inArr ) > 0 ) {
795
796            // clean for ints
797            $inArrChecked = array();
798            foreach ( $inArr as $x ) {
799                $inArrChecked[] = (int) $x;
800            }
801
802            // add where
803            $wheres['inarray'] = array( 'ID', 'IN', '(' . implode( ',', $inArrChecked ) . ')' );
804
805        }
806
807        #} Owned by
808        if ( ! empty( $ownedBy ) && $ownedBy > 0 ) {
809
810            // would never hard-type this in (would make generic as in buildWPMetaQueryWhere)
811            // but this is only here until MIGRATED to db2 globally
812            // $wheres['incompany'] = array('ID','IN','(SELECT DISTINCT post_id FROM '.$wpdb->prefix."postmeta WHERE meta_key = 'zbs_company' AND meta_value = %d)",$inCompany);
813            // Use obj links now
814            $wheres['ownedBy'] = array( 'zbs_owner', '=', '%s', $ownedBy );
815
816        }
817
818        // External sources
819        if ( ! empty( $externalSource ) ) {
820
821            // NO owernship built into this, check when roll out multi-layered ownsership
822            $wheres['externalsource'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_QUOTE . ' AND zbss_source = %s)', $externalSource );
823
824        }
825
826        // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
827        // quick addition for mike
828        #} olderThan
829        if ( ! empty( $olderThan ) && $olderThan > 0 ) {
830            $wheres['olderThan'] = array( 'zbsq_created', '<=', '%d', $olderThan );
831        }
832        #} newerThan
833        if ( ! empty( $newerThan ) && $newerThan > 0 ) {
834            $wheres['newerThan'] = array( 'zbsq_created', '>=', '%d', $newerThan );
835        }
836
837        // status
838        if ( ! empty( $hasAccepted ) && $hasAccepted ) {
839            $wheres['hasAccepted'] = array( 'zbsq_accepted', '>', '%d', 0 );
840        }
841        if ( ! empty( $hasNotAccepted ) && $hasNotAccepted ) {
842            $wheres['hasnotAccepted'] = array( 'zbsq_accepted', '<=', '%d', 0 );
843        }
844
845        // assignedContact + assignedCompany
846        if ( ! empty( $assignedContact ) && $assignedContact > 0 ) {
847            $wheres['assignedContact'] = array( 'ID', 'IN', '( SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = %d)', $assignedContact );
848        }
849        if ( ! empty( $assignedCompany ) && $assignedCompany > 0 ) {
850            $wheres['assignedCompany'] = array( 'ID', 'IN', '( SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_to = %d)', $assignedCompany );
851        }
852        // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
853
854        #} Quick filters - adapted from DAL1 (probs can be slicker)
855        if ( is_array( $quickFilters ) && count( $quickFilters ) > 0 ) {
856
857            // cycle through
858            foreach ( $quickFilters as $quick_filter ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
859
860                if ( $quick_filter === 'status_accepted' ) {
861                    $wheres['quickfilterstatus']  = array( 'zbsq_accepted', '>', '0' );
862                    $wheres['quickfilterstatus2'] = array( 'zbsq_template', '>', '0' );
863                } elseif ( $quick_filter === 'status_notaccepted' ) {
864                    $wheres['quickfilterstatus']  = array( 'zbsq_accepted', '<=', '0' );
865                    $wheres['quickfilterstatus2'] = array( 'zbsq_template', '>', '0' );
866                } elseif ( $quick_filter === 'status_draft' ) {
867                    $wheres['quickfilterstatus'] = array( 'zbsq_template', '<=', '0' );
868                }
869            }
870        } // / quickfilters
871
872        #} Is Tagged (expects 1 tag ID OR array)
873
874        // catch 1 item arr
875        if ( is_array( $isTagged ) && count( $isTagged ) == 1 ) {
876            $isTagged = $isTagged[0];
877        }
878
879        if ( ! empty( $isTagged ) && ! is_array( $isTagged ) && $isTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
880
881                // add where tagged
882                // 1 int:
883                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = quote.ID AND zbstl_tagid = %d) > 0)', array( ZBS_TYPE_QUOTE, $isTagged ) );
884
885        } elseif ( is_array( $isTagged ) && count( $isTagged ) > 0 ) {
886
887            // foreach in array :)
888            $tagStr = '';
889            foreach ( $isTagged as $iTag ) {
890                $i = (int) $iTag;
891                if ( $i > 0 ) {
892
893                    if ( $tagStr !== '' ) {
894                        $tagStr .= ',';
895                    }
896                    $tagStr .= $i;
897                }
898            }
899            if ( ! empty( $tagStr ) ) {
900
901                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = quote.ID AND zbstl_tagid IN (%s)) > 0)', array( ZBS_TYPE_QUOTE, $tagStr ) );
902
903            }
904        }
905        #} Is NOT Tagged (expects 1 tag ID OR array)
906
907        // catch 1 item arr
908        if ( is_array( $isNotTagged ) && count( $isNotTagged ) == 1 ) {
909            $isNotTagged = $isNotTagged[0];
910        }
911
912        if ( ! empty( $isNotTagged ) && ! is_array( $isNotTagged ) && $isNotTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
913
914            // add where tagged
915            // 1 int:
916            $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = quote.ID AND zbstl_tagid = %d) = 0)', array( ZBS_TYPE_QUOTE, $isNotTagged ) );
917
918        } elseif ( is_array( $isNotTagged ) && count( $isNotTagged ) > 0 ) {
919
920            // foreach in array :)
921            $tagStr = '';
922            foreach ( $isNotTagged as $iTag ) {
923                $i = (int) $iTag;
924                if ( $i > 0 ) {
925
926                    if ( $tagStr !== '' ) {
927                        $tagStr .= ',';
928                    }
929                    $tagStr .= $i;
930                }
931            }
932            if ( ! empty( $tagStr ) ) {
933
934                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = quote.ID AND zbstl_tagid IN (%s)) = 0)', array( ZBS_TYPE_QUOTE, $tagStr ) );
935
936            }
937        }
938
939        #} ============ / WHERE ===============
940
941        #} ============   SORT   ==============
942
943        // Obj Model based sort conversion
944        // converts 'addr1' => 'zbsco_addr1' generically
945        if ( isset( $this->objectModel[ $sortByField ] ) && isset( $this->objectModel[ $sortByField ]['fieldname'] ) ) {
946            $sortByField = $this->objectModel[ $sortByField ]['fieldname'];
947        }
948
949        // Mapped sorts
950        // This catches listview and other exception sort cases
951        $sort_map = array(
952            'customer' => '(SELECT zbsol_objid_to FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_QUOTE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_from = quote.ID)', // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
953            // hack to support quote statuses (for here only, 2 is accepted, 1 is unaccepted, 0 is draft)
954            'status'   => '(CASE WHEN zbsq_accepted > 0 THEN 2 WHEN zbsq_template > 0 THEN 1 ELSE 0 END)',
955        );
956
957        if ( array_key_exists( $sortByField, $sort_map ) ) {
958
959            $sortByField = $sort_map[ $sortByField ];
960
961        }
962
963        #} ============ / SORT   ==============
964
965        #} CHECK this + reset to default if faulty
966        if ( ! in_array( $whereCase, array( 'AND', 'OR' ) ) ) {
967            $whereCase = 'AND';
968        }
969
970        #} Build out any WHERE clauses
971        $wheresArr = $this->buildWheres( $wheres, $whereStr, $params, $whereCase );
972        $whereStr  = $wheresArr['where'];
973        $params    = $params + $wheresArr['params'];
974        #} / Build WHERE
975
976        #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
977        $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
978        $ownQ   = $this->ownershipSQL( $ignoreowner, 'quote' );
979        if ( ! empty( $ownQ ) ) {
980            $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
981        }
982        #} / Ownership
983
984        #} Append to sql (this also automatically deals with sortby and paging)
985        $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
986
987        try {
988
989            #} Prep & run query
990            $queryObj = $this->prepare( $query, $params );
991
992            #} Catch count + return if requested
993            if ( $count ) {
994                return $wpdb->get_var( $queryObj );
995            }
996
997            #} else continue..
998            $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
999
1000        } catch ( Exception $e ) {
1001
1002            #} General SQL Err
1003            $this->catchSQLError( $e );
1004
1005        }
1006
1007        #} Interpret results (Result Set - multi-row)
1008        if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
1009
1010            #} Has results, tidy + return
1011            foreach ( $potentialRes as $resDataLine ) {
1012
1013                // using onlyColumns filter?
1014                if ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
1015
1016                    // only coumns return.
1017                    $resArr = array();
1018                    foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
1019
1020                        if ( isset( $resDataLine->$colDBKey ) ) {
1021                            $resArr[ $colStr ] = $resDataLine->$colDBKey;
1022                        }
1023                    }
1024                } else {
1025
1026                    // tidy
1027                    $resArr = $this->tidy_quote( $resDataLine, $withCustomFields );
1028
1029                }
1030
1031                if ( $withLineItems ) {
1032
1033                    // add all line item lines
1034                    $resArr['lineitems'] = $this->DAL()->lineitems->getLineitems(
1035                        array(
1036                            'associatedObjType' => ZBS_TYPE_QUOTE,
1037                            'associatedObjID'   => $resDataLine->ID,
1038                            'perPage'           => 1000,
1039                            'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
1040                        )
1041                    );
1042
1043                }
1044
1045                if ( $withFiles ) {
1046
1047                    $resArr['files'] = zeroBSCRM_files_getFiles( 'quote', $resDataLine->ID );
1048
1049                }
1050
1051                if ( $withTags ) {
1052
1053                    // add all tags lines
1054                    $resArr['tags'] = $this->DAL()->getTagsForObjID(
1055                        array(
1056                            'objtypeid' => ZBS_TYPE_QUOTE,
1057                            'objid'     => $resDataLine->ID,
1058                        )
1059                    );
1060
1061                }
1062
1063                if ( $withAssigned ) {
1064
1065                    /*
1066                    This is for MULTIPLE (e.g. multi contact/companies assigned to an inv)
1067
1068                        // add all assigned contacts/companies
1069                        $res['contacts'] = $this->DAL()->contacts->getContacts(array(
1070                    'hasObjTypeLinkedTo'=>ZBS_TYPE_QUOTE,
1071                    'hasObjIDLinkedTo'=>$resDataLine->ID,
1072                    'perPage'=>-1,
1073                    'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_CONTACT)));
1074
1075                        $res['companies'] = $this->DAL()->companies->getCompanies(array(
1076                    'hasObjTypeLinkedTo'=>ZBS_TYPE_QUOTE,
1077                    'hasObjIDLinkedTo'=>$resDataLine->ID,
1078                    'perPage'=>-1,
1079                    'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_COMPANY)));
1080
1081                    .. but we use 1:1, at least now: */
1082
1083                    // add all assigned contacts/companies
1084                    $resArr['contact'] = $this->DAL()->contacts->getContacts(
1085                        array(
1086                            'hasObjTypeLinkedTo' => ZBS_TYPE_QUOTE,
1087                            'hasObjIDLinkedTo'   => $resDataLine->ID,
1088                            'page'               => 0,
1089                            'perPage'            => 1, // FORCES 1
1090                            'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
1091                        )
1092                    );
1093
1094                    $resArr['company'] = $this->DAL()->companies->getCompanies(
1095                        array(
1096                            'hasObjTypeLinkedTo' => ZBS_TYPE_QUOTE,
1097                            'hasObjIDLinkedTo'   => $resDataLine->ID,
1098                            'page'               => 0,
1099                            'perPage'            => 1, // FORCES 1
1100                            'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_COMPANY ),
1101                        )
1102                    );
1103
1104                }
1105
1106                if ( $suppressContent ) {
1107                    unset( $resArr['content'] );
1108                }
1109
1110                $res[] = $resArr;
1111
1112            }
1113        }
1114
1115        return $res;
1116    }
1117
1118    /**
1119     * Returns a count of quotes (owned)
1120     * .. inc by status
1121     *
1122     * @return int count
1123     */
1124    public function getQuoteCount( $args = array() ) {
1125
1126        #} ============ LOAD ARGS =============
1127        $defaultArgs = array(
1128
1129            // Search/Filtering (leave as false to ignore)
1130            'withStatus'  => false, // will be str if used
1131
1132            // permissions
1133            'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_QUOTE ), // this'll let you not-check the owner of obj
1134
1135        );
1136        foreach ( $defaultArgs as $argK => $argV ) {
1137            $$argK = $argV;
1138            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1139                if ( is_array( $args[ $argK ] ) ) {
1140                    $newData = $$argK;
1141                    if ( ! is_array( $newData ) ) {
1142                        $newData = array();
1143                    }
1144                    foreach ( $args[ $argK ] as $subK => $subV ) {
1145                        $newData[ $subK ] = $subV;
1146                    }
1147                    $$argK = $newData;
1148                } else {
1149                    $$argK = $args[ $argK ];
1150                }
1151            }
1152        }
1153        #} =========== / LOAD ARGS =============
1154
1155        $whereArr = array();
1156
1157        if ( $withStatus !== false && ! empty( $withStatus ) ) {
1158            $whereArr['status'] = array( 'zbsq_status', '=', '%s', $withStatus );
1159        }
1160
1161        return $this->DAL()->getFieldByWHERE(
1162            array(
1163                'objtype'     => ZBS_TYPE_QUOTE,
1164                'colname'     => 'COUNT(ID)',
1165                'where'       => $whereArr,
1166                'ignoreowner' => $ignoreowner,
1167            )
1168        );
1169    }
1170
1171    /**
1172     * adds or updates a quote object
1173     *
1174     * @param array $args Associative array of arguments
1175     *              id (if update), owner, data (array of field data)
1176     *
1177     * @return int line ID
1178     */
1179    public function addUpdateQuote( $args = array() ) {
1180
1181        global $ZBSCRM_t, $wpdb, $zbs;
1182
1183        #} Retrieve any cf
1184        $customFields     = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_QUOTE ) );
1185        $addrCustomFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_ADDRESS ) );
1186
1187        #} ============ LOAD ARGS =============
1188        $defaultArgs = array(
1189
1190            'id'                   => -1,
1191            'owner'                => -1,
1192
1193            // fields (directly)
1194            'data'                 => array(
1195
1196                'id_override'      => '',
1197                'title'            => '',
1198                'currency'         => '',
1199                'value'            => '',
1200                'date'             => '',
1201                'template'         => '',
1202                'content'          => '',
1203                'notes'            => '',
1204                'send_attachments' => -1,
1205                'hash'             => '',
1206                'lastviewed'       => '',
1207                'viewed_count'     => '',
1208                'accepted'         => '',
1209                'acceptedsigned'   => '',
1210                'acceptedip'       => '',
1211
1212                // lineitems:
1213                'lineitems'        => false,
1214                // will be an array of lineitem lines (as per matching lineitem database model)
1215                // note:    if no change desired, pass "false"
1216                // if removal of all/change, pass empty array
1217
1218                // Note Custom fields may be passed here, but will not have defaults so check isset()
1219
1220                // obj links:
1221                'contacts'         => false, // array of id's
1222                'companies'        => false, // array of id's
1223
1224                // tags
1225                'tags'             => -1, // pass an array of tag ids or tag strings
1226                'tag_mode'         => 'replace', // replace|append|remove
1227
1228                'externalSources'  => -1, // if this is an array(array('source'=>src,'uid'=>uid),multiple()) it'll add :)
1229
1230                // allow this to be set for MS sync, Migrations etc.
1231                'created'          => -1,
1232                'lastupdated'      => '',
1233
1234            ),
1235
1236            'limitedFields'        => -1, // if this is set it OVERRIDES data (allowing you to set specific fields + leave rest in tact)
1237            // ^^ will look like: array(array('key'=>x,'val'=>y,'type'=>'%s'))
1238
1239            // this function as DAL1 func did.
1240            'extraMeta'            => -1,
1241            'automatorPassthrough' => -1,
1242
1243            'silentInsert'         => false, // this was for init Migration - it KILLS all IA for newQuote (because is migrating, not creating new :) this was -1 before
1244
1245            'do_not_update_blanks' => false, // this allows you to not update fields if blank (same as fieldoverride for extsource -> in)
1246
1247        );
1248        foreach ( $defaultArgs as $argK => $argV ) {
1249            $$argK = $argV;
1250            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1251                if ( is_array( $args[ $argK ] ) ) {
1252                    $newData = $$argK;
1253                    if ( ! is_array( $newData ) ) {
1254                        $newData = array();
1255                    }
1256                    foreach ( $args[ $argK ] as $subK => $subV ) {
1257                        $newData[ $subK ] = $subV;
1258                    }
1259                    $$argK = $newData;
1260                } else {
1261                    $$argK = $args[ $argK ];
1262                }
1263            }
1264        }
1265
1266        // Needs this to grab custom fields (if passed) too :)
1267        if ( is_array( $customFields ) ) {
1268            foreach ( $customFields as $cK => $cF ) {
1269
1270                // only for data, limited fields below
1271                if ( is_array( $data ) ) {
1272
1273                    if ( isset( $args['data'][ $cK ] ) ) {
1274                        $data[ $cK ] = $args['data'][ $cK ];
1275                    }
1276                }
1277            }
1278        }
1279
1280        // this takes limited fields + checks through for custom fields present
1281        // (either as key zbsq_source or source, for example)
1282        // then switches them into the $data array, for separate update
1283        // where this'll fall over is if NO normal contact data is sent to update, just custom fields
1284        if ( is_array( $limitedFields ) && is_array( $customFields ) ) {
1285
1286            // $customFieldKeys = array_keys($customFields);
1287            $newLimitedFields = array();
1288
1289            // cycle through
1290            foreach ( $limitedFields as $field ) {
1291
1292                // some weird case where getting empties, so added check
1293                if ( isset( $field['key'] ) && ! empty( $field['key'] ) ) {
1294
1295                    $dePrefixed = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1296                    if ( str_starts_with( $field['key'], 'zbsq_' ) ) {
1297                        $dePrefixed = substr( $field['key'], strlen( 'zbsq_' ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1298                    }
1299
1300                    if ( isset( $customFields[ $field['key'] ] ) ) {
1301
1302                        // is custom, move to data
1303                        $data[ $field['key'] ] = $field['val'];
1304
1305                    } elseif ( ! empty( $dePrefixed ) && isset( $customFields[ $dePrefixed ] ) ) {
1306
1307                        // is custom, move to data
1308                        $data[ $dePrefixed ] = $field['val'];
1309
1310                    } else {
1311
1312                        // add it to limitedFields (it's not dealt with post-update)
1313                        $newLimitedFields[] = $field;
1314                    }
1315                }
1316            }
1317
1318            // move this back in
1319            $limitedFields = $newLimitedFields;
1320            unset( $newLimitedFields );
1321
1322        }
1323
1324        #} =========== / LOAD ARGS ============
1325
1326        #} ========== CHECK FIELDS ============
1327
1328        $id = (int) $id;
1329
1330        // here we check that the potential owner CAN even own
1331        if ( ! user_can( $owner, 'admin_zerobs_usr' ) ) {
1332            $owner = -1;
1333        }
1334
1335        // if owner = -1, add current
1336        if ( ! isset( $owner ) || $owner === -1 ) {
1337            $owner = zeroBSCRM_user();
1338        }
1339
1340        // if no date is passed, use current date
1341        if ( empty( $data['date'] ) ) {
1342            $data['date'] = time(); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1343        }
1344
1345        if ( is_array( $limitedFields ) ) {
1346
1347            // LIMITED UPDATE (only a few fields.)
1348            if ( ! is_array( $limitedFields ) || count( $limitedFields ) <= 0 ) {
1349                return false;
1350            }
1351            // REQ. ID too (can only update)
1352            if ( empty( $id ) || $id <= 0 ) {
1353                return false;
1354            }
1355        } else {
1356
1357            // NORMAL, FULL UPDATE
1358
1359        }
1360
1361        #} ========= / CHECK FIELDS ===========
1362
1363        #} ========= OVERRIDE SETTING (Deny blank overrides) ===========
1364
1365        // this only functions if externalsource is set (e.g. api/form, etc.)
1366        if ( isset( $data['externalSources'] ) && is_array( $data['externalSources'] ) && count( $data['externalSources'] ) > 0 ) {
1367            if ( zeroBSCRM_getSetting( 'fieldoverride' ) == '1' ) {
1368
1369                $do_not_update_blanks = true;
1370
1371            }
1372        }
1373
1374        // either ext source + setting, or set by the func call
1375        if ( $do_not_update_blanks ) {
1376
1377            // this setting says 'don't override filled-out data with blanks'
1378            // so here we check through any passed blanks + convert to limitedFields
1379            // only matters if $id is set (there is somt to update not add
1380            if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
1381
1382                // get data to copy over (for now, this is required to remove 'fullname' etc.)
1383                $dbData = $this->db_ready_quote( $data );
1384                // unset($dbData['id']); // this is unset because we use $id, and is update, so not req. legacy issue
1385                // 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 :)
1386
1387                $origData    = $data; // $data = array();
1388                $limitedData = array(); // array(array('key'=>'zbsq_x','val'=>y,'type'=>'%s'))
1389
1390                // cycle through + translate into limitedFields (removing any blanks, or arrays (e.g. externalSources))
1391                // we also have to remake a 'faux' data (removing blanks for tags etc.) for the post-update updates
1392                foreach ( $dbData as $k => $v ) {
1393
1394                    $intV = (int) $v;
1395
1396                    // only add if valuenot empty
1397                    if ( ! is_array( $v ) && ! empty( $v ) && $v != '' && $v !== 0 && $v !== -1 && $intV !== -1 ) {
1398
1399                        // add to update arr
1400                        $limitedData[] = array(
1401                            'key'  => 'zbsq_' . $k, // we have to add zbsq_ here because translating from data -> limited fields
1402                            'val'  => $v,
1403                            'type' => $this->getTypeStr( 'zbsq_' . $k ),
1404                        );
1405
1406                        // add to remade $data for post-update updates
1407                        $data[ $k ] = $v;
1408
1409                    }
1410                }
1411
1412                // copy over
1413                $limitedFields = $limitedData;
1414
1415            } // / if ID
1416
1417        } // / if do_not_update_blanks
1418
1419        #} ========= / OVERRIDE SETTING (Deny blank overrides) ===========
1420
1421        #} ========= BUILD DATA ===========
1422
1423        $update              = false;
1424        $dataArr             = array();
1425        $typeArr             = array();
1426        $was_accepted_before = $this->getQuoteAccepted( $id );
1427
1428        if ( is_array( $limitedFields ) ) {
1429
1430            // LIMITED FIELDS
1431            $update = true;
1432
1433            // cycle through
1434            foreach ( $limitedFields as $field ) {
1435
1436                // some weird case where getting empties, so added check
1437                if ( ! empty( $field['key'] ) ) {
1438                    $dataArr[ $field['key'] ] = $field['val'];
1439                    $typeArr[]                = $field['type'];
1440                }
1441            }
1442
1443            // add update time
1444            if ( ! isset( $dataArr['zbsq_lastupdated'] ) ) {
1445                $dataArr['zbsq_lastupdated'] = time();
1446                $typeArr[]                   = '%d';
1447            }
1448        } else {
1449
1450            // FULL UPDATE/INSERT
1451
1452            // contacts - avoid dupes
1453            if ( isset( $data['contacts'] ) && is_array( $data['contacts'] ) ) {
1454
1455                $coArr = array();
1456                foreach ( $data['contacts'] as $c ) {
1457                    $cI = (int) $c;
1458                    if ( $cI > 0 && ! in_array( $cI, $coArr ) ) {
1459                        $coArr[] = $cI;
1460                    }
1461                }
1462
1463                // reset the main
1464                if ( count( $coArr ) > 0 ) {
1465                    $data['contacts'] = $coArr;
1466                } else {
1467                    $data['contacts'] = 'unset';
1468                }
1469                unset( $coArr );
1470
1471            }
1472
1473            // companies - avoid dupes
1474            if ( isset( $data['companies'] ) && is_array( $data['companies'] ) ) {
1475
1476                $coArr = array();
1477                foreach ( $data['companies'] as $c ) {
1478                    $cI = (int) $c;
1479                    if ( $cI > 0 && ! in_array( $cI, $coArr ) ) {
1480                        $coArr[] = $cI;
1481                    }
1482                }
1483
1484                // reset the main
1485                if ( count( $coArr ) > 0 ) {
1486                    $data['companies'] = $coArr;
1487                } else {
1488                    $data['companies'] = 'unset';
1489                }
1490                unset( $coArr );
1491
1492            }
1493
1494            // UPDATE
1495            $dataArr = array(
1496
1497                // ownership
1498                // no need to update these (as of yet) - can't move teams etc.
1499                // 'zbs_site' => zeroBSCRM_installSite(),
1500                // 'zbs_team' => zeroBSCRM_installTeam(),
1501                // 'zbs_owner' => $owner,
1502
1503                'zbsq_id_override'      => $data['id_override'],
1504                'zbsq_title'            => $data['title'],
1505                'zbsq_currency'         => $data['currency'],
1506                'zbsq_value'            => $data['value'],
1507                'zbsq_date'             => $data['date'],
1508                'zbsq_template'         => $data['template'],
1509                'zbsq_content'          => $data['content'],
1510                'zbsq_notes'            => $data['notes'],
1511                'zbsq_send_attachments' => $data['send_attachments'],
1512                'zbsq_hash'             => $data['hash'],
1513                'zbsq_lastviewed'       => $data['lastviewed'],
1514                'zbsq_viewed_count'     => $data['viewed_count'],
1515                'zbsq_accepted'         => $data['accepted'],
1516                'zbsq_acceptedsigned'   => $data['acceptedsigned'],
1517                'zbsq_acceptedip'       => $data['acceptedip'],
1518                'zbsq_lastupdated'      => time(),
1519
1520            );
1521
1522            $typeArr = array( // field data types
1523                            // '%d',  // site
1524                            // '%d',  // team
1525                            // '%d',  // owner
1526
1527                '%s',
1528                '%s',
1529                '%s',
1530                '%s',
1531                '%d',
1532                '%s',
1533                '%s',
1534                '%s',
1535                '%d', // send_attachment
1536                '%s',
1537                '%d',
1538                '%d',
1539                '%d',
1540                '%s',
1541                '%s',
1542                '%d',
1543
1544            );
1545
1546            if ( ! empty( $id ) && $id > 0 ) {
1547
1548                // is update
1549                $update = true;
1550
1551            } else {
1552
1553                // INSERT (get's few extra :D)
1554                $update               = false;
1555                $dataArr['zbs_site']  = zeroBSCRM_site();
1556                $typeArr[]            = '%d';
1557                $dataArr['zbs_team']  = zeroBSCRM_team();
1558                $typeArr[]            = '%d';
1559                $dataArr['zbs_owner'] = $owner;
1560                $typeArr[]            = '%d';
1561                if ( isset( $data['created'] ) && ! empty( $data['created'] ) && $data['created'] !== -1 ) {
1562                    $dataArr['zbsq_created'] = $data['created'];
1563                    $typeArr[]               = '%d';
1564                } else {
1565                    $dataArr['zbsq_created'] = time();
1566                    $typeArr[]               = '%d';
1567                }
1568
1569                // and on new inserts, if no hash passed, it gen's one
1570                if ( isset( $dataArr['zbsq_hash'] ) && $dataArr['zbsq_hash'] == '' ) {
1571                    $dataArr['zbsq_hash'] = zeroBSCRM_generateHash( 20 );
1572                }
1573            }
1574        }
1575
1576        #} ========= / BUILD DATA ===========
1577
1578        #} ============================================================
1579        #} ========= CHECK force_uniques & not_empty & max_len ========
1580
1581        // if we're passing limitedFields we skip these, for now
1582        // #v3.1 - would make sense to unique/nonempty check just the limited fields. #gh-145
1583        if ( ! is_array( $limitedFields ) ) {
1584
1585            // verify uniques
1586            if ( ! $this->verifyUniqueValues( $data, $id ) ) {
1587                return false; // / fails unique field verify
1588            }
1589
1590            // verify not_empty
1591            if ( ! $this->verifyNonEmptyValues( $data ) ) {
1592                return false; // / fails empty field verify
1593            }
1594        }
1595
1596        // whatever we do we check for max_len breaches and abbreviate to avoid wpdb rejections
1597        $dataArr = $this->wpdbChecks( $dataArr );
1598
1599        #} ========= / CHECK force_uniques & not_empty ================
1600        #} ============================================================
1601
1602        #} Check if ID present
1603        if ( $update ) {
1604
1605            /*
1606            WH included this "for now" - status change for IA, not sure if used for this obj,?
1607
1608                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
1609                $originalStatus = $this->getQuoteStatus($id);
1610
1611                // log any change of status
1612                if ( isset( $dataArr['zbsq_status']) && !empty( $dataArr['zbsq_status']) && !empty( $originalStatus) && $dataArr['zbsq_status'] != $originalStatus){
1613
1614                    // status change
1615                    $statusChange = array(
1616                        'from' => $originalStatus,
1617                        'to' => $dataArr['zbsq_status']
1618                        );
1619                }
1620            */
1621
1622            #} Attempt update
1623            if ( $wpdb->update(
1624                $ZBSCRM_t['quotes'],
1625                $dataArr,
1626                array( // where
1627                    'ID' => $id,
1628                ),
1629                $typeArr,
1630                array( // where data types
1631                    '%d',
1632                )
1633            ) !== false ) {
1634
1635                // if passing limitedFields instead of data, we ignore the following
1636                    // this doesn't work, because data is in args default as arr
1637                    // if (isset($data) && is_array($data)){
1638                    // so...
1639                if ( ! isset( $limitedFields ) || ! is_array( $limitedFields ) || $limitedFields == -1 ) {
1640
1641                    // Line Items ====
1642
1643                    // line item work
1644                    if ( isset( $data['lineitems'] ) && is_array( $data['lineitems'] ) ) {
1645
1646                        // if array passed, update, even if removing
1647                        if ( count( $data['lineitems'] ) > 0 ) {
1648
1649                            // passed, for now this is BRUTAL and just clears old ones + readds
1650                            // once live, discuss how to refactor to be less brutal.
1651
1652                            // delete all lineitems
1653                            $this->DAL()->lineitems->deleteLineItemsForObject(
1654                                array(
1655                                    'objID'   => $id,
1656                                    'objType' => ZBS_TYPE_QUOTE,
1657                                )
1658                            );
1659
1660                            // addupdate each
1661                            foreach ( $data['lineitems'] as $lineitem ) {
1662
1663                                // slight rejig of passed so works cleanly with data array style
1664                                $lineItemID = false;
1665                                if ( isset( $lineitem['ID'] ) ) {
1666                                    $lineItemID = $lineitem['ID'];
1667                                }
1668                                $this->DAL()->lineitems->addUpdateLineitem(
1669                                    array(
1670                                        'id'            => $lineItemID,
1671                                        'linkedObjType' => ZBS_TYPE_QUOTE,
1672                                        'linkedObjID'   => $id,
1673                                        'data'          => $lineitem,
1674                                    )
1675                                );
1676
1677                            }
1678                        } else {
1679
1680                            // delete all lineitems
1681                            $this->DAL()->lineitems->deleteLineItemsForObject(
1682                                array(
1683                                    'objID'   => $id,
1684                                    'objType' => ZBS_TYPE_QUOTE,
1685                                )
1686                            );
1687
1688                        }
1689                    }
1690
1691                    // / Line Items ====
1692
1693                    // OBJ LINKS - to contacts/companies
1694                    $this->addUpdateObjectLinks( $id, $data['contacts'], ZBS_TYPE_CONTACT ); // 13567
1695                    $this->addUpdateObjectLinks( $id, $data['companies'], ZBS_TYPE_COMPANY );
1696                    // IA also gets 'againstid' historically, but we'll pass as 'against id's'
1697                    $againstIDs = array(
1698                        'contacts'  => $data['contacts'],
1699                        'companies' => $data['companies'],
1700                    );
1701
1702                    // tags
1703                    if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
1704
1705                        $this->addUpdateQuoteTags(
1706                            array(
1707                                'id'        => $id,
1708                                'tag_input' => $data['tags'],
1709                                'mode'      => $data['tag_mode'],
1710                            )
1711                        );
1712
1713                    }
1714
1715                    // externalSources
1716                    $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
1717                        array(
1718                            'obj_id'           => $id,
1719                            'obj_type_id'      => ZBS_TYPE_QUOTE,
1720                            'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
1721                        )
1722                    ); // for IA below
1723
1724                    // Custom fields?
1725
1726                    #} Cycle through + add/update if set
1727                    if ( is_array( $customFields ) ) {
1728                        foreach ( $customFields as $cK => $cF ) {
1729
1730                            // any?
1731                            if ( isset( $data[ $cK ] ) ) {
1732
1733                                // add update
1734                                $cfID = $this->DAL()->addUpdateCustomField(
1735                                    array(
1736                                        'data' => array(
1737                                            'objtype' => ZBS_TYPE_QUOTE,
1738                                            'objid'   => $id,
1739                                            'objkey'  => $cK,
1740                                            'objval'  => $data[ $cK ],
1741                                        ),
1742                                    )
1743                                );
1744
1745                            }
1746                        }
1747                    }
1748
1749                    // / Custom Fields
1750
1751                } // / if $data
1752
1753                #} Any extra meta keyval pairs?
1754                // BRUTALLY updates (no checking)
1755                $confirmedExtraMeta = false;
1756                if ( is_array( $extraMeta ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1757
1758                    $confirmedExtraMeta = array();
1759
1760                    foreach ( $extraMeta as $k => $v ) {
1761
1762                        #} This won't fix stupid keys, just catch basic fails...
1763                        $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
1764
1765                        #} Brutal update
1766                        // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
1767                        $this->DAL()->updateMeta( ZBS_TYPE_QUOTE, $id, 'extra_' . $cleanKey, $v );
1768
1769                        #} Add it to this, which passes to IA
1770                        $confirmedExtraMeta[ $cleanKey ] = $v;
1771
1772                    }
1773                }
1774
1775                #} INTERNAL AUTOMATOR
1776                #} &
1777                #} FALLBACKS
1778                // UPDATING CONTACT
1779                if ( ! $silentInsert ) {
1780
1781                    // IA General quote update (2.87+)
1782                    zeroBSCRM_FireInternalAutomator(
1783                        'quote.update',
1784                        array(
1785                            'id'                   => $id,
1786                            'data'                 => $data,
1787                            'againstids'           => array(), // $againstIDs,
1788                            'extsource'            => false, // $approvedExternalSource,
1789                            'automatorpassthrough' => $automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
1790                            'extraMeta'            => $confirmedExtraMeta, #} This is the "extraMeta" passed (as saved)
1791                        )
1792                    );
1793
1794                    // First check if the quote status is new before fire the event
1795                    if ( empty( $was_accepted_before ) && $data['accepted'] ) {
1796
1797                        // IA quote.accepted
1798                        zeroBSCRM_FireInternalAutomator(
1799                            'quote.accepted',
1800                            array(
1801                                'id'                   => $id,
1802                                'data'                 => $data,
1803                                'againstids'           => false,
1804                                'extsource'            => false,
1805                                'automatorpassthrough' => false,
1806                                'extraMeta'            => $confirmedExtraMeta,
1807                            )
1808                        );
1809                    }
1810                }
1811
1812                // Successfully updated - Return id
1813                return $id;
1814
1815            } else {
1816
1817                $msg = __( 'DB Update Failed', 'zero-bs-crm' );
1818                $zbs->DAL->addError( 302, $this->objectType, $msg, $dataArr );
1819
1820                // FAILED update
1821                return false;
1822
1823            }
1824        } else {
1825
1826            #} No ID - must be an INSERT
1827            if ( $wpdb->insert(
1828                $ZBSCRM_t['quotes'],
1829                $dataArr,
1830                $typeArr
1831            ) > 0 ) {
1832
1833                #} Successfully inserted, lets return new ID
1834                $newID = $wpdb->insert_id;
1835
1836                // Line Items ====
1837
1838                // line item work
1839                if ( isset( $data['lineitems'] ) && is_array( $data['lineitems'] ) ) {
1840
1841                    // if array passed, update, even if removing
1842                    if ( count( $data['lineitems'] ) > 0 ) {
1843
1844                        // passed, for now this is BRUTAL and just clears old ones + readds
1845                        // once live, discuss how to refactor to be less brutal.
1846
1847                        // delete all lineitems
1848                        $this->DAL()->lineitems->deleteLineItemsForObject(
1849                            array(
1850                                'objID'   => $newID,
1851                                'objType' => ZBS_TYPE_QUOTE,
1852                            )
1853                        );
1854
1855                        // addupdate each
1856                        foreach ( $data['lineitems'] as $lineitem ) {
1857
1858                            // slight rejig of passed so works cleanly with data array style
1859                            $lineItemID = false;
1860                            if ( isset( $lineitem['ID'] ) ) {
1861                                $lineItemID = $lineitem['ID'];
1862                            }
1863                            $this->DAL()->lineitems->addUpdateLineitem(
1864                                array(
1865                                    'id'            => $lineItemID,
1866                                    'linkedObjType' => ZBS_TYPE_QUOTE,
1867                                    'linkedObjID'   => $newID,
1868                                    'data'          => $lineitem,
1869                                )
1870                            );
1871
1872                        }
1873                    } else {
1874
1875                        // delete all lineitems
1876                        $this->DAL()->lineitems->deleteLineItemsForObject(
1877                            array(
1878                                'objID'   => $newID,
1879                                'objType' => ZBS_TYPE_QUOTE,
1880                            )
1881                        );
1882
1883                    }
1884                }
1885
1886                // / Line Items ====
1887
1888                // OBJ LINKS - to contacts/companies
1889                $this->addUpdateObjectLinks( $newID, $data['contacts'], ZBS_TYPE_CONTACT );
1890                $this->addUpdateObjectLinks( $newID, $data['companies'], ZBS_TYPE_COMPANY );
1891                // IA also gets 'againstid' historically, but we'll pass as 'against id's'
1892                $againstIDs = array(
1893                    'contacts'  => $data['contacts'],
1894                    'companies' => $data['companies'],
1895                );
1896
1897                // tags
1898                if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
1899
1900                    $this->addUpdateQuoteTags(
1901                        array(
1902                            'id'        => $newID,
1903                            'tag_input' => $data['tags'],
1904                            'mode'      => $data['tag_mode'],
1905                        )
1906                    );
1907
1908                }
1909
1910                // externalSources
1911                $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
1912                    array(
1913                        'obj_id'           => $newID,
1914                        'obj_type_id'      => ZBS_TYPE_QUOTE,
1915                        'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
1916                    )
1917                ); // for IA below
1918
1919                // Custom fields?
1920
1921                #} Cycle through + add/update if set
1922                if ( is_array( $customFields ) ) {
1923                    foreach ( $customFields as $cK => $cF ) {
1924
1925                        // any?
1926                        if ( isset( $data[ $cK ] ) ) {
1927
1928                            // add update
1929                            $cfID = $this->DAL()->addUpdateCustomField(
1930                                array(
1931                                    'data' => array(
1932                                        'objtype' => ZBS_TYPE_QUOTE,
1933                                        'objid'   => $newID,
1934                                        'objkey'  => $cK,
1935                                        'objval'  => $data[ $cK ],
1936                                    ),
1937                                )
1938                            );
1939
1940                        }
1941                    }
1942                }
1943
1944                // / Custom Fields
1945
1946                #} Any extra meta keyval pairs?
1947                // BRUTALLY updates (no checking)
1948                $confirmedExtraMeta = false;
1949                if ( is_array( $extraMeta ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1950
1951                    $confirmedExtraMeta = array();
1952
1953                    foreach ( $extraMeta as $k => $v ) {
1954
1955                        #} This won't fix stupid keys, just catch basic fails...
1956                        $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
1957
1958                        #} Brutal update
1959                        // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
1960                        $this->DAL()->updateMeta( ZBS_TYPE_QUOTE, $newID, 'extra_' . $cleanKey, $v );
1961
1962                        #} Add it to this, which passes to IA
1963                        $confirmedExtraMeta[ $cleanKey ] = $v;
1964
1965                    }
1966                }
1967
1968                #} INTERNAL AUTOMATOR
1969                #} &
1970                #} FALLBACKS
1971                // NEW CONTACT
1972                if ( ! $silentInsert ) {
1973
1974                    #} Add to automator
1975                    zeroBSCRM_FireInternalAutomator(
1976                        'quote.new',
1977                        array(
1978                            'id'                   => $newID,
1979                            'data'                 => $data,
1980                            'againstids'           => $againstIDs,
1981                            'extsource'            => $approvedExternalSource,
1982                            'automatorpassthrough' => $automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
1983                            'extraMeta'            => $confirmedExtraMeta, #} This is the "extraMeta" passed (as saved)
1984                        )
1985                    );
1986
1987                }
1988
1989                return $newID;
1990
1991            } else {
1992
1993                $msg = __( 'DB Insert Failed', 'zero-bs-crm' );
1994                $zbs->DAL->addError( 303, $this->objectType, $msg, $dataArr );
1995
1996                #} Failed to Insert
1997                return false;
1998
1999            }
2000        }
2001
2002        return false;
2003    }
2004
2005    /**
2006     * adds or updates a quote's tags
2007     * ... this is really just a wrapper for addUpdateObjectTags
2008     *
2009     * @param array $args Associative array of arguments
2010     *              id (if update), owner, data (array of field data)
2011     *
2012     * @return int line ID
2013     */
2014    public function addUpdateQuoteTags( $args = array() ) {
2015
2016        global $ZBSCRM_t, $wpdb;
2017
2018        #} ============ LOAD ARGS =============
2019        $defaultArgs = array(
2020
2021            'id'        => -1,
2022
2023            // generic pass-through (array of tag strings or tag IDs):
2024            'tag_input' => -1,
2025
2026            // or either specific:
2027            'tagIDs'    => -1,
2028            'tags'      => -1,
2029
2030            'mode'      => 'append',
2031
2032        );
2033        foreach ( $defaultArgs as $argK => $argV ) {
2034            $$argK = $argV;
2035            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2036                if ( is_array( $args[ $argK ] ) ) {
2037                    $newData = $$argK;
2038                    if ( ! is_array( $newData ) ) {
2039                        $newData = array();
2040                    }
2041                    foreach ( $args[ $argK ] as $subK => $subV ) {
2042                        $newData[ $subK ] = $subV;
2043                    }
2044                    $$argK = $newData;
2045                } else {
2046                    $$argK = $args[ $argK ];
2047                }
2048            }
2049        }
2050        #} =========== / LOAD ARGS ============
2051
2052        #} ========== CHECK FIELDS ============
2053
2054        // check id
2055        $id = (int) $id;
2056        if ( empty( $id ) || $id <= 0 ) {
2057            return false;
2058        }
2059
2060        #} ========= / CHECK FIELDS ===========
2061
2062        return $this->DAL()->addUpdateObjectTags(
2063            array(
2064                'objtype'   => ZBS_TYPE_QUOTE,
2065                'objid'     => $id,
2066                'tag_input' => $tag_input,
2067                'tags'      => $tags,
2068                'tagIDs'    => $tagIDs,
2069                'mode'      => $mode,
2070            )
2071        );
2072    }
2073
2074    /**
2075     * updates template for a quote
2076     *
2077     * @param int id quote ID
2078     * @param int template ID
2079     *
2080     * @return bool
2081     */
2082    public function setQuoteTemplate( $id = -1, $template = -1 ) {
2083
2084        global $zbs;
2085
2086        $id = (int) $id;
2087
2088        if ( $id > 0 ) {
2089
2090            return $this->addUpdateQuote(
2091                array(
2092                    'id'            => $id,
2093                    'limitedFields' => array(
2094                        array(
2095                            'key'  => 'zbsq_template',
2096                            'val'  => $template,
2097                            'type' => '%d',
2098                        ),
2099                    ),
2100                )
2101            );
2102
2103        }
2104
2105        return false;
2106    }
2107
2108    /**
2109     * accepts/unaccepts a quote
2110     * ... this is really just a wrapper for addUpdateQuote
2111     * ... and replaces zeroBS_markQuoteAccepted + zeroBS_markQuoteUnAccepted
2112     *
2113     * @param array $args Associative array of arguments
2114     *              id (if update), owner, data (array of field data)
2115     *
2116     * @return int line ID
2117     */
2118    public function addUpdateQuoteAccepted( $args = array() ) {
2119
2120        global $ZBSCRM_t, $wpdb;
2121
2122        #} ============ LOAD ARGS =============
2123        $defaultArgs = array(
2124
2125            'id'       => -1,
2126            'accepted' => -1,
2127            'signedby' => '',
2128            'ip'       => '',
2129
2130        );
2131        foreach ( $defaultArgs as $argK => $argV ) {
2132            $$argK = $argV;
2133            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2134                if ( is_array( $args[ $argK ] ) ) {
2135                    $newData = $$argK;
2136                    if ( ! is_array( $newData ) ) {
2137                        $newData = array();
2138                    }
2139                    foreach ( $args[ $argK ] as $subK => $subV ) {
2140                        $newData[ $subK ] = $subV;
2141                    }
2142                    $$argK = $newData;
2143                } else {
2144                    $$argK = $args[ $argK ];
2145                }
2146            }
2147        }
2148        #} =========== / LOAD ARGS ============
2149
2150        #} ========== CHECK FIELDS ============
2151
2152        // check id
2153        $id = (int) $id;
2154        if ( empty( $id ) || $id <= 0 ) {
2155            return false;
2156        }
2157
2158        // WPID may be -1 (NULL)
2159        // -1 does okay here if ($WPID == -1) $WPID = '';
2160
2161        #} ========= / CHECK FIELDS ===========
2162
2163        #} Enact
2164        $r = $this->addUpdateQuote(
2165            array(
2166                'id'            => $id,
2167                'limitedFields' => array(
2168                    array(
2169                        'key'  => 'zbsq_accepted',
2170                        'val'  => $accepted,
2171                        'type' => '%d',
2172                    ),
2173                    array(
2174                        'key'  => 'zbsq_acceptedsigned',
2175                        'val'  => $signedby,
2176                        'type' => '%s',
2177                    ),
2178                    array(
2179                        'key'  => 'zbsq_acceptedip',
2180                        'val'  => $ip,
2181                        'type' => '%s',
2182                    ),
2183                ),
2184            )
2185        );
2186
2187        // if quote was accepted, run internal automator quote.accepted
2188        if ( $accepted ) {
2189
2190            // fire automator
2191            zeroBSCRM_FireInternalAutomator(
2192                'quote.accepted',
2193                array(
2194                    'id'                   => $id,
2195                    'data'                 => array(
2196                        'signed' => $signedby,
2197                        'ip'     => $ip,
2198                    ),
2199                    'againstids'           => false,
2200                    'extsource'            => false,
2201                    'automatorpassthrough' => false,
2202                    'extraMeta'            => false,
2203                )
2204            );
2205
2206        }
2207
2208        return $r;
2209    }
2210
2211    /**
2212     * updates status for an quote (no blanks allowed)
2213     *
2214     * @param int id quote ID
2215     * @param string quote Status
2216     *
2217     * @return bool
2218     */
2219    public function setQuoteStatus( $id = -1, $status = -1 ) {
2220
2221        global $zbs;
2222
2223        $id = (int) $id;
2224
2225        if ( $id > 0 && ! empty( $status ) && $status !== -1 ) {
2226
2227            return $this->addUpdateQuote(
2228                array(
2229                    'id'            => $id,
2230                    'limitedFields' => array(
2231                        array(
2232                            'key'  => 'zbsq_status',
2233                            'val'  => $status,
2234                            'type' => '%s',
2235                        ),
2236                    ),
2237                )
2238            );
2239
2240        }
2241
2242        return false;
2243    }
2244
2245    /**
2246     * deletes a quote object
2247     *
2248     * @param array $args Associative array of arguments
2249     *              id
2250     *
2251     * @return int success;
2252     */
2253    public function deleteQuote( $args = array() ) {
2254
2255        global $ZBSCRM_t, $wpdb, $zbs;
2256
2257        #} ============ LOAD ARGS =============
2258        $defaultArgs = array(
2259
2260            'id'          => -1,
2261            'saveOrphans' => false,
2262
2263        );
2264        foreach ( $defaultArgs as $argK => $argV ) {
2265            $$argK = $argV;
2266            if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2267                if ( is_array( $args[ $argK ] ) ) {
2268                    $newData = $$argK;
2269                    if ( ! is_array( $newData ) ) {
2270                        $newData = array();
2271                    }
2272                    foreach ( $args[ $argK ] as $subK => $subV ) {
2273                        $newData[ $subK ] = $subV;
2274                    }
2275                    $$argK = $newData;
2276                } else {
2277                    $$argK = $args[ $argK ];
2278                }
2279            }
2280        }
2281        #} =========== / LOAD ARGS ============
2282
2283        #} Check ID & Delete :)
2284        $id = (int) $id;
2285        if ( ! empty( $id ) && $id > 0 ) {
2286
2287            // delete orphans?
2288            if ( $saveOrphans === false ) {
2289
2290                // delete any tag links
2291                $this->DAL()->deleteTagObjLinks(
2292                    array(
2293
2294                        'objtype' => ZBS_TYPE_QUOTE,
2295                        'objid'   => $id,
2296                    )
2297                );
2298
2299                // delete any external source information
2300                $this->DAL()->delete_external_sources(
2301                    array(
2302
2303                        'obj_type'   => ZBS_TYPE_FORM,
2304                        'obj_id'     => $id,
2305                        'obj_source' => 'all',
2306
2307                    )
2308                );
2309
2310            }
2311
2312            $del = zeroBSCRM_db2_deleteGeneric( $id, 'quotes' );
2313
2314            #} Add to automator
2315            zeroBSCRM_FireInternalAutomator(
2316                'quote.delete',
2317                array(
2318                    'id'          => $id,
2319                    'saveOrphans' => $saveOrphans,
2320                )
2321            );
2322
2323            return $del;
2324
2325        }
2326
2327        return false;
2328    }
2329
2330    /**
2331     * tidy's the object from wp db into clean array
2332     *
2333     * @param array $obj (DB obj)
2334     *
2335     * @return array quote (clean obj)
2336     */
2337    public function tidy_quote( $obj = false, $withCustomFields = false ) { // phpcs:ignore
2338
2339        $res = false;
2340
2341        if ( isset( $obj->ID ) ) {
2342            $res       = array();
2343            $res['id'] = $obj->ID;
2344            /*
2345                `zbs_site` INT NULL DEFAULT NULL,
2346                `zbs_team` INT NULL DEFAULT NULL,
2347                `zbs_owner` INT NOT NULL,
2348            */
2349            $res['owner'] = $obj->zbs_owner;
2350
2351            $res['id_override']      = $this->stripSlashes( $obj->zbsq_id_override );
2352            $res['title']            = $this->stripSlashes( $obj->zbsq_title );
2353            $res['currency']         = $this->stripSlashes( $obj->zbsq_currency );
2354            $res['value']            = $this->stripSlashes( $obj->zbsq_value );
2355            $res['date']             = (int) $obj->zbsq_date;
2356            $res['date_date']        = ( isset( $obj->zbsq_date ) && $obj->zbsq_date > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsq_date, false, true ) : false;
2357            $res['template']         = (int) $this->stripSlashes( $obj->zbsq_template );
2358            $res['content']          = $this->stripSlashes( $obj->zbsq_content );
2359            $res['notes']            = $this->stripSlashes( $obj->zbsq_notes );
2360            $res['send_attachments'] = (bool) $obj->zbsq_send_attachments;
2361            $res['hash']             = $this->stripSlashes( $obj->zbsq_hash );
2362            $res['lastviewed']       = (int) $obj->zbsq_lastviewed;
2363            $res['lastviewed_date']  = ( isset( $obj->zbsq_lastviewed ) && $obj->zbsq_lastviewed > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsq_lastviewed, false, true ) : false;
2364            $res['viewed_count']     = (int) $obj->zbsq_viewed_count;
2365            $res['accepted']         = (int) $obj->zbsq_accepted;
2366            $res['accepted_date']    = ( isset( $obj->zbsq_accepted ) && $obj->zbsq_accepted > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsq_accepted, false, true ) : false;
2367            $res['acceptedsigned']   = (int) $obj->zbsq_accepted;
2368            $res['acceptedip']       = (int) $obj->zbsq_accepted;
2369            $res['created']          = (int) $obj->zbsq_created;
2370            $res['created_date']     = ( isset( $obj->zbsq_created ) && $obj->zbsq_created > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsq_created, false, true ) : false;
2371            $res['lastupdated']      = (int) $obj->zbsq_lastupdated;
2372            $res['lastupdated_date'] = ( isset( $obj->zbsq_lastupdated ) && $obj->zbsq_lastupdated > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsq_lastupdated, false, true ) : false;
2373
2374            // determine status :)
2375            $res['status'] = -1; // not yet pub
2376            if ( isset( $res['template'] ) && $res['template'] > 0 ) {
2377                $res['status'] = -2; // published not accepted
2378            }
2379            if ( isset( $res['accepted'] ) && $res['accepted'] > 0 ) {
2380                $res['status'] = 1; // accepted
2381            }
2382
2383            // custom fields - tidy any that are present:
2384            if ( $withCustomFields ) {
2385                $res = $this->tidyAddCustomFields( ZBS_TYPE_QUOTE, $obj, $res, false );
2386            }
2387        }
2388
2389        return $res;
2390    }
2391
2392    /**
2393     * Wrapper, use $this->getQuoteMeta($contactID,$key) for easy retrieval of singular quote
2394     * Simplifies $this->getMeta
2395     *
2396     * @param int objtype
2397     * @param int objid
2398     * @param string key
2399     *
2400     * @return array quote meta result
2401     */
2402    public function getQuoteMeta( $id = -1, $key = '', $default = false ) {
2403
2404        global $zbs;
2405
2406        if ( ! empty( $key ) ) {
2407
2408            return $this->DAL()->getMeta(
2409                array(
2410
2411                    'objtype'     => ZBS_TYPE_QUOTE,
2412                    'objid'       => $id,
2413                    'key'         => $key,
2414                    'fullDetails' => false,
2415                    'default'     => $default,
2416                    'ignoreowner' => true, // for now !!
2417
2418                )
2419            );
2420
2421        }
2422
2423        return $default;
2424    }
2425
2426    /**
2427     * Returns content body against a quote
2428     *
2429     * @param int id quote ID
2430     *
2431     * @return int quote owner id
2432     */
2433    public function getQuoteContent( $id = -1 ) {
2434
2435        global $zbs;
2436
2437        $id = (int) $id;
2438
2439        if ( $id > 0 ) {
2440
2441            return $this->DAL()->getFieldByID(
2442                array(
2443                    'id'          => $id,
2444                    'objtype'     => ZBS_TYPE_QUOTE,
2445                    'colname'     => 'zbsq_content',
2446                    'ignoreowner' => true,
2447                )
2448            );
2449
2450        }
2451
2452        return false;
2453    }
2454
2455    /**
2456     * Returns quote template ID, or 0
2457     *
2458     * @param int id quote ID
2459     *
2460     * @return int quote template ID, or 0
2461     */
2462    public function getQuoteTemplateID( $id = -1 ) {
2463
2464        global $zbs;
2465
2466        $id = (int) $id;
2467
2468        if ( $id > 0 ) {
2469
2470            return $this->DAL()->getFieldByID(
2471                array(
2472                    'id'          => $id,
2473                    'objtype'     => ZBS_TYPE_QUOTE,
2474                    'colname'     => 'zbsq_template',
2475                    'ignoreowner' => true,
2476                )
2477            );
2478
2479        }
2480
2481        return false;
2482    }
2483
2484    /**
2485     * Returns quote accepted UTC time, if accepted
2486     *
2487     * @param int id quote ID
2488     *
2489     * @return int UTC accepted, or 0
2490     */
2491    public function getQuoteAcceptedTime( $id = -1 ) {
2492
2493        global $zbs;
2494
2495        $id = (int) $id;
2496
2497        if ( $id > 0 ) {
2498
2499            return $this->DAL()->getFieldByID(
2500                array(
2501                    'id'          => $id,
2502                    'objtype'     => ZBS_TYPE_QUOTE,
2503                    'colname'     => 'zbsq_accepted',
2504                    'ignoreowner' => true,
2505                )
2506            );
2507
2508        }
2509
2510        return false;
2511    }
2512
2513    /**
2514     * Returns quote accepted details, if accepted
2515     *
2516     * @param int id quote ID
2517     *
2518     * @return array (Accepted deets)
2519     */
2520    public function getQuoteAccepted( $id = -1 ) {
2521
2522        global $zbs;
2523
2524        $id = (int) $id;
2525
2526        if ( $id > 0 ) {
2527
2528            $ret = array(
2529
2530                'accepted'       => $this->DAL()->getFieldByID(
2531                    array(
2532                        'id'          => $id,
2533                        'objtype'     => ZBS_TYPE_QUOTE,
2534                        'colname'     => 'zbsq_accepted',
2535                        'ignoreowner' => true,
2536                    )
2537                ),
2538
2539                'acceptedsigned' => $this->DAL()->getFieldByID(
2540                    array(
2541                        'id'          => $id,
2542                        'objtype'     => ZBS_TYPE_QUOTE,
2543                        'colname'     => 'zbsq_acceptedsigned',
2544                        'ignoreowner' => true,
2545                    )
2546                ),
2547
2548                'acceptedip'     => $this->DAL()->getFieldByID(
2549                    array(
2550                        'id'          => $id,
2551                        'objtype'     => ZBS_TYPE_QUOTE,
2552                        'colname'     => 'zbsq_acceptedip',
2553                        'ignoreowner' => true,
2554                    )
2555                ),
2556
2557            );
2558
2559        }
2560
2561        return false;
2562    }
2563
2564    /**
2565     * Returns an ownerid against a quote
2566     *
2567     * @param int id quote ID
2568     *
2569     * @return int quote owner id
2570     */
2571    public function getQuoteOwner( $id = -1 ) {
2572
2573        global $zbs;
2574
2575        $id = (int) $id;
2576
2577        if ( $id > 0 ) {
2578
2579            return $this->DAL()->getFieldByID(
2580                array(
2581                    'id'          => $id,
2582                    'objtype'     => ZBS_TYPE_QUOTE,
2583                    'colname'     => 'zbs_owner',
2584                    'ignoreowner' => true,
2585                )
2586            );
2587
2588        }
2589
2590        return false;
2591    }
2592
2593    /**
2594     * Returns the first contact associated with a quote
2595     *
2596     * @param int id quote ID
2597     *
2598     * @return int quote contact id
2599     */
2600    public function getQuoteContactID( $id = -1 ) {
2601
2602        global $zbs;
2603
2604        $id = (int) $id;
2605
2606        if ( $id > 0 ) {
2607
2608            $contacts = $this->DAL()->getObjsLinkedToObj(
2609                array(
2610
2611                    'objtypefrom' => ZBS_TYPE_QUOTE,
2612                    'objtypeto'   => ZBS_TYPE_CONTACT,
2613                    'objfromid'   => $id,
2614                    'count'       => false,
2615                )
2616            );
2617
2618            if ( is_array( $contacts ) ) {
2619                foreach ( $contacts as $c ) {
2620
2621                    // first
2622                    return $c['id'];
2623
2624                }
2625            }
2626        }
2627
2628        return false;
2629    }
2630
2631    /**
2632     * Returns an hash against a quote
2633     *
2634     * @param int $id quote ID.
2635     *
2636     * @return string quote hash string
2637     */
2638    public function getQuoteHash( $id = -1 ) {
2639
2640        global $zbs;
2641
2642        $id = (int) $id;
2643
2644        if ( $id > 0 ) {
2645
2646            return $this->DAL()->getFieldByID(
2647                array(
2648                    'id'          => $id,
2649                    'objtype'     => ZBS_TYPE_QUOTE,
2650                    'colname'     => 'zbsq_hash',
2651                    'ignoreowner' => true,
2652                )
2653            );
2654
2655        }
2656
2657        return false;
2658    }
2659
2660    /**
2661     * remove any non-db fields from the object
2662     * basically takes array like array('owner'=>1,'fname'=>'x','fullname'=>'x')
2663     * and returns array like array('owner'=>1,'fname'=>'x')
2664     * This does so based on the objectModel!
2665     *
2666     * @param array $obj (clean obj)
2667     *
2668     * @return array (db ready arr)
2669     */
2670    private function db_ready_quote( $obj = false ) {
2671
2672        // use the generic? (override here if necessary)
2673        return $this->db_ready_obj( $obj );
2674    }
2675
2676    /**
2677     * Takes full object and makes a "list view" boiled down version
2678     * Used to generate listview objs
2679     *
2680     * @param array $obj (clean obj)
2681     *
2682     * @return array (listview ready obj)
2683     */
2684    public function listViewObj( $quote = false, $columnsRequired = array() ) {
2685
2686        if ( is_array( $quote ) && isset( $quote['id'] ) ) {
2687
2688            $resArr = $quote;
2689
2690            if ( isset( $quote['id_override'] ) && $quote['id_override'] !== null ) {
2691                $resArr['zbsid'] = $quote['id_override'];
2692            } else {
2693                $resArr['zbsid'] = $quote['id'];
2694            }
2695
2696            #} Custom columns
2697
2698            #} Status
2699            $resArr['statusint'] = -2; // def
2700
2701            // determine status - this is now done in tidy_quote
2702            if ( isset( $quote['status'] ) ) {
2703                $resArr['statusint'] = $quote['status'];
2704            }
2705
2706            if ( $resArr['statusint'] == -2 ) {
2707
2708                #} is published
2709                $resArr['status'] = '<span class="ui label orange">' . __( 'Not accepted yet', 'zero-bs-crm' ) . '</span>';
2710
2711            } elseif ( $resArr['statusint'] == -1 ) {
2712
2713                #} not yet published
2714                $resArr['status'] = '<span class="ui label grey">' . __( 'Draft', 'zero-bs-crm' ) . '</span>';
2715
2716            } else {
2717
2718                #} Accepted
2719                $resArr['status'] = '<span class="ui label green">' . __( 'Accepted', 'zero-bs-crm' ) . ' ' . date( zeroBSCRM_getDateFormat(), $quote['accepted'] ) . '</span>';
2720
2721            }
2722
2723            #} customer
2724            if ( in_array( 'customer', $columnsRequired ) ) {
2725
2726                #} Convert $contact arr into list-view-digestable 'customer'// & unset contact for leaner data transfer
2727                $resArr['customer'] = zeroBSCRM_getSimplyFormattedContact( $quote['contact'], ( in_array( 'assignedobj', $columnsRequired ) ) );
2728
2729                #} Convert $contact arr into list-view-digestable 'customer'// & unset contact for leaner data transfer
2730                // not yet. $resArr['company'] = zeroBSCRM_getSimplyFormattedCompany($transaction['company'],(in_array('assignedobj', $columnsRequired)));
2731
2732            }
2733
2734            // let it use proper obj.
2735            // $resArr['added'] = $quote['created_date'];
2736            // prev was 'val'
2737            $resArr['value'] = zeroBSCRM_formatCurrency( $resArr['value'] );
2738
2739            return $resArr;
2740
2741        }
2742
2743        return false;
2744    }
2745
2746    // ===========  /   QUOTE  =======================================================
2747    // ===============================================================================
2748} // / class