Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1339
0.00% covered (danger)
0.00%
0 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
zbsDAL_invoices
0.00% covered (danger)
0.00%
0 / 1339
0.00% covered (danger)
0.00%
0 / 27
180200
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 add_listview_filters
0.00% covered (danger)
0.00%
0 / 11
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
 getInvoice
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 1
1722
 getInvoices
0.00% covered (danger)
0.00%
0 / 245
0.00% covered (danger)
0.00%
0 / 1
9702
 getInvoiceCount
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 addUpdateInvoice
0.00% covered (danger)
0.00%
0 / 469
0.00% covered (danger)
0.00%
0 / 1
14762
 addUpdateInvoiceTags
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 setInvoiceStatus
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 deleteInvoice
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
110
 tidy_invoice
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
306
 recalculate
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
812
 generateTotalsTable
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
870
 getInvoiceMeta
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getInvoiceOwner
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getInvoiceContactID
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getInvoiceContact
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getInvoiceCompany
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getInvoiceStatus
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_invoice_status_except_deleted_for_query
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getInvoiceHash
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getOutstandingBalance
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
156
 db_ready_invoice
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 / 17
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * V3.0+
6 *
7 * Copyright 2020 Automattic
8 *
9 * Date: 14/01/19
10 */
11
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14use Automattic\Jetpack\CRM\Event_Manager\Events_Manager;
15
16/**
17 * ZBS DAL >> Invoices
18 *
19 * @author   Woody Hayday <hello@jetpackcrm.com>
20 * @version  2.0
21 * @access   public
22 * @see      https://jetpackcrm.com/kb
23 */
24class zbsDAL_invoices extends zbsDAL_ObjectLayer {
25
26    protected $objectType            = ZBS_TYPE_INVOICE;
27    protected $objectDBPrefix        = 'zbsi_';
28    protected $include_in_templating = true;
29    protected $objectModel           = array(
30
31        /*
32            NOTE:
33
34                $zbsCustomerInvoiceFields Removed as of v3.0, invoice builder is very custom, UI wise,
35                .. and as the model can deal with saving + custom fields WITHOUT the global, there's no need
36                (whereas other objects input views are directed by these globals, Invs is separate, way MS made it)
37
38            OLD hard-typed:
39
40                $zbsCustomerInvoiceFields = array(
41
42                    'status' => array(
43                        'select', 'Status','',array(
44                            'Draft', 'Unpaid', 'Paid', 'Overdue', 'Deleted'
45                        ), 'essential' => true
46                    ),
47
48                                    # NOTE! 'no' should now be ignored, (deprecated), moved to separate meta 'zbsid'
49
50                    // NOTE WH: when I hit this with column manager, loads didn't need to be shown
51                    // so plz leave ,'nocolumn'=>true in tact :)
52
53                    //'name' => array('text','Quote title','e.g. Chimney Rebuild'),
54                    'no' => array('text',__('Invoice number',"zero-bs-crm"),'e.g. 123456', 'essential' => true), #} No is ignored by edit routines :)
55                    'val'=> array('hidden',__('Invoice value',"zero-bs-crm"),'e.g. 500.00', 'essential' => true),
56                    'date' => array('date',__('Invoice date',"zero-bs-crm"),'', 'essential' => true),
57                    'notes' => array('textarea',__('Notes',"zero-bs-crm"),'','nocolumn'=>true),
58                    'ref' => array('text', __('Reference number',"zero-bs-crm"), 'e.g. Ref-123'),
59                    'due' => array('text', __('Invoice due',"zero-bs-crm"), ''),
60                    'logo' => array('text', __('logo url',"zero-bs-crm"), 'e.g. URL','nocolumn'=>true),
61
62                    'bill' => array('text',__('invoice to',"zero-bs-crm"), 'e.g. mike@epicplugins.com','nocolumn'=>true),
63                    'ccbill' => array('text',__('copy invoice to',"zero-bs-crm"), 'e.g. you@you.com','nocolumn'=>true),
64
65                );
66
67        */
68
69        // ID
70        'ID'                  => array(
71            'fieldname' => 'ID',
72            'format'    => 'int',
73        ),
74
75        // site + team generics
76        'zbs_site'            => array(
77            'fieldname' => 'zbs_site',
78            'format'    => 'int',
79        ),
80        'zbs_team'            => array(
81            'fieldname' => 'zbs_team',
82            'format'    => 'int',
83        ),
84        'zbs_owner'           => array(
85            'fieldname' => 'zbs_owner',
86            'format'    => 'int',
87        ),
88
89        // other fields
90        'id_override'         => array(
91            'fieldname'    => 'zbsi_id_override',
92            'format'       => 'str',
93            'force_unique' => true, // must be unique. This is required and breaking if true
94            'can_be_blank' => true, // can be blank (if not unique)
95            'max_len'      => 128,
96        ),
97        'parent'              => array(
98            'fieldname' => 'zbsi_parent',
99            'format'    => 'int',
100        ),
101        'status'              => array(
102            'fieldname' => 'zbsi_status',
103            'format'    => 'str',
104            'max_len'   => 50,
105        ),
106        'hash'                => array(
107            'fieldname' => 'zbsi_hash',
108            'format'    => 'str',
109        ),
110        'pdf_template'        => array(
111            'fieldname' => 'zbsi_pdf_template',
112            'format'    => 'str',
113        ),
114        'portal_template'     => array(
115            'fieldname' => 'zbsi_portal_template',
116            'format'    => 'str',
117        ),
118        'email_template'      => array(
119            'fieldname' => 'zbsi_email_template',
120            'format'    => 'str',
121        ),
122        'invoice_frequency'   => array(
123            'fieldname' => 'zbsi_invoice_frequency',
124            'format'    => 'int',
125        ),
126        'currency'            => array(
127            'fieldname' => 'zbsi_currency',
128            'format'    => 'curr',
129        ),
130        'pay_via'             => array(
131            'fieldname' => 'zbsi_pay_via',
132            'format'    => 'int',
133        ),
134        /*
135            -1 = bacs/can'tpay online
136                0 = default/no setting
137                1 = paypal
138                2 = stripe
139                3 = worldpay
140            */
141        'logo_url'            => array(
142            'fieldname' => 'zbsi_logo_url',
143            'format'    => 'str',
144            'max_len'   => 300,
145        ),
146        'address_to_objtype'  => array(
147            'fieldname' => 'zbsi_address_to_objtype',
148            'format'    => 'int',
149        ),
150        'addressed_from'      => array(
151            'fieldname' => 'zbsi_addressed_from',
152            'format'    => 'str',
153            'max_len'   => 600,
154        ),
155        'addressed_to'        => array(
156            'fieldname' => 'zbsi_addressed_to',
157            'format'    => 'str',
158            'max_len'   => 600,
159        ),
160        'allow_partial'       => array(
161            'fieldname' => 'zbsi_allow_partial',
162            'format'    => 'bool',
163        ),
164        'allow_tip'           => array(
165            'fieldname' => 'zbsi_allow_tip',
166            'format'    => 'bool',
167        ),
168        'send_attachments'    => array(
169            'fieldname' => 'zbsi_send_attachments',
170            'format'    => 'bool',
171        ), // 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
172        'hours_or_quantity'   => array(
173            'fieldname' => 'zbsi_hours_or_quantity',
174            'format'    => 'bool',
175        ),
176        'date'                => array(
177            'fieldname' => 'zbsi_date',
178            'format'    => 'uts',
179        ),
180        'due_date'            => array(
181            'fieldname' => 'zbsi_due_date',
182            'format'    => 'uts',
183        ),
184        'paid_date'           => array(
185            'fieldname' => 'zbsi_paid_date',
186            'format'    => 'uts',
187        ),
188        'hash_viewed'         => array(
189            'fieldname' => 'zbsi_hash_viewed',
190            'format'    => 'uts',
191        ),
192        'hash_viewed_count'   => array(
193            'fieldname' => 'zbsi_hash_viewed_count',
194            'format'    => 'int',
195        ),
196        'portal_viewed'       => array(
197            'fieldname' => 'zbsi_portal_viewed',
198            'format'    => 'uts',
199        ),
200        'portal_viewed_count' => array(
201            'fieldname' => 'zbsi_portal_viewed_count',
202            'format'    => 'int',
203        ),
204        'net'                 => array(
205            'fieldname' => 'zbsi_net',
206            'format'    => 'decimal',
207        ),
208        'discount'            => array(
209            'fieldname' => 'zbsi_discount',
210            'format'    => 'decimal',
211        ),
212        'discount_type'       => array(
213            'fieldname' => 'zbsi_discount_type',
214            'format'    => 'str',
215        ),
216        'shipping'            => array(
217            'fieldname' => 'zbsi_shipping',
218            'format'    => 'decimal',
219        ),
220        'shipping_taxes'      => array(
221            'fieldname' => 'zbsi_shipping_taxes',
222            'format'    => 'str',
223        ),
224        'shipping_tax'        => array(
225            'fieldname' => 'zbsi_shipping_tax',
226            'format'    => 'decimal',
227        ),
228        'taxes'               => array(
229            'fieldname' => 'zbsi_taxes',
230            'format'    => 'str',
231        ),
232        'tax'                 => array(
233            'fieldname' => 'zbsi_tax',
234            'format'    => 'decimal',
235        ),
236        'total'               => array(
237            'fieldname' => 'zbsi_total',
238            'format'    => 'decimal',
239        ),
240        'created'             => array(
241            'fieldname' => 'zbsi_created',
242            'format'    => 'uts',
243        ),
244        'lastupdated'         => array(
245            'fieldname' => 'zbsi_lastupdated',
246            'format'    => 'uts',
247        ),
248
249    );
250
251        // hardtyped list of types this object type is commonly linked to
252        protected $linkedToObjectTypes = array(
253
254            ZBS_TYPE_CONTACT,
255            ZBS_TYPE_COMPANY,
256
257        );
258
259        /**
260         * Events_Manager instance. Manages CRM events.
261         *
262         * @since 6.2.0
263         *
264         * @var Events_Manager
265         */
266        private $events_manager;
267
268        function __construct( $args = array() ) {
269
270            #} =========== LOAD ARGS ==============
271            $defaultArgs = array(
272
273            // 'tag' => false,
274
275            );
276            foreach ( $defaultArgs as $argK => $argV ) {
277                $this->$argK = $argV;
278                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
279                    if ( is_array( $args[ $argK ] ) ) {
280                        $newData = $this->$argK;
281                        if ( ! is_array( $newData ) ) {
282                            $newData = array();
283                        } foreach ( $args[ $argK ] as $subK => $subV ) {
284                            $newData[ $subK ] = $subV;
285                        }$this->$argK = $newData;
286                    } else {
287                        $this->$argK = $args[ $argK ]; }
288                }
289            }
290            #} =========== / LOAD ARGS =============
291
292            $this->events_manager = new Events_Manager();
293
294            add_filter( 'jpcrm_listview_filters', array( $this, 'add_listview_filters' ) );
295        }
296
297        /**
298         * Adds items to listview filter using `jpcrm_listview_filters` hook.
299         *
300         * @param array $listview_filters Listview filters.
301         */
302        public function add_listview_filters( $listview_filters ) {
303            global $zbs;
304            // Add statuses if enabled.
305            if ( $zbs->settings->get( 'filtersfromstatus' ) === 1 ) {
306                $statuses = array(
307                    'Draft'   => __( 'Draft', 'zero-bs-crm' ),
308                    'Unpaid'  => __( 'Unpaid', 'zero-bs-crm' ),
309                    'Paid'    => __( 'Paid', 'zero-bs-crm' ),
310                    'Overdue' => __( 'Overdue', 'zero-bs-crm' ),
311                    'Deleted' => __( 'Deleted', 'zero-bs-crm' ),
312                );
313                foreach ( $statuses as $status_slug => $status_label ) {
314                    $listview_filters[ ZBS_TYPE_INVOICE ]['status'][ 'status_' . $status_slug ] = $status_label;
315                }
316            }
317            return $listview_filters;
318        }
319
320        // ===============================================================================
321        // ===========   INVOICE  =======================================================
322
323        // generic get Company (by ID)
324        // Super simplistic wrapper used by edit page etc. (generically called via dal->contacts->getSingle etc.)
325        public function getSingle( $ID = -1 ) {
326
327            return $this->getInvoice( $ID );
328        }
329
330        // generic get (by ID list)
331        // Super simplistic wrapper used by MVP Export v3.0
332        public function getIDList( $IDs = false ) {
333
334            return $this->getInvoices(
335                array(
336                    'inArr'        => $IDs,
337                    'withOwner'    => true,
338                    'withAssigned' => true,
339                    'page'         => -1,
340                    'perPage'      => -1,
341                )
342            );
343        }
344
345        // generic get (EVERYTHING)
346        // expect heavy load!
347        public function getAll( $IDs = false ) {
348
349            return $this->getInvoices(
350                array(
351                    'withOwner'    => true,
352                    'withAssigned' => true,
353                    'sortByField'  => 'ID',
354                    'sortOrder'    => 'ASC',
355                    'page'         => -1,
356                    'perPage'      => -1,
357                )
358            );
359        }
360
361        // generic get count of (EVERYTHING)
362        public function getFullCount() {
363
364            return $this->getInvoices(
365                array(
366                    'count'   => true,
367                    'page'    => -1,
368                    'perPage' => -1,
369                )
370            );
371        }
372
373        /**
374         * returns full invoice line +- details
375         *
376         * @param int id        invoice id
377         * @param array                    $args   Associative array of arguments
378         *
379         * @return array invoice object
380         */
381        public function getInvoice( $id = -1, $args = array() ) {
382
383            global $zbs;
384
385            #} =========== LOAD ARGS ==============
386            $defaultArgs = array(
387
388                // if theset wo passed, will search based on these
389                'idOverride'        => false, // directly checks 1:1 match id_override
390                'externalSource'    => false,
391                'externalSourceUID' => false,
392                'hash'              => false,
393
394                // with what?
395                'withLineItems'     => true,
396                'withCustomFields'  => true,
397                'withTransactions'  => false, // gets trans associated with inv as well
398                'withAssigned'      => false, // return ['contact'] & ['company'] objs if has link
399                'withTags'          => false,
400                'withOwner'         => false,
401                'withFiles'         => false,
402                'withTotals'        => false, // uses $this->generateTotalsTable to also calc discount + taxes on fly
403
404            // permissions
405                'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_INVOICE ), // this'll let you not-check the owner of obj
406
407            // returns scalar ID of line
408                'onlyID'            => false,
409
410                'fields'            => false, // false = *, array = fieldnames
411
412            );
413            foreach ( $defaultArgs as $argK => $argV ) {
414                $$argK = $argV;
415                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
416                    if ( is_array( $args[ $argK ] ) ) {
417                        $newData = $$argK;
418                        if ( ! is_array( $newData ) ) {
419                            $newData = array();
420                        } foreach ( $args[ $argK ] as $subK => $subV ) {
421                            $newData[ $subK ] = $subV;
422                        }$$argK = $newData;
423                    } else {
424                        $$argK = $args[ $argK ]; }
425                }
426            }
427            #} =========== / LOAD ARGS =============
428
429            #} Check ID
430            $id = (int) $id;
431            if (
432            ( ! empty( $id ) && $id > 0 )
433            ||
434            ( ! empty( $email ) )
435            ||
436            ( ! empty( $hash ) )
437            ||
438            ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) )
439            ) {
440
441                global $ZBSCRM_t, $wpdb;
442                $wheres          = array( 'direct' => array() );
443                $whereStr        = '';
444                $additionalWhere = '';
445                $params          = array();
446                $res             = array();
447                $extraSelect     = '';
448
449                #} ============= PRE-QUERY ============
450
451                #} Custom Fields
452                if ( $withCustomFields && ! $onlyID ) {
453
454                    #} Retrieve any cf
455                    $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_INVOICE ) );
456
457                    #} Cycle through + build into query
458                    if ( is_array( $custFields ) ) {
459                        foreach ( $custFields as $cK => $cF ) {
460
461                            // add as subquery
462                            $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . " WHERE zbscf_objid = invoice.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) '" . $cK . "'";
463
464                            // add params
465                            $params[] = $cK;
466                            $params[] = ZBS_TYPE_INVOICE;
467
468                        }
469                    }
470                }
471
472                $selector = 'invoice.*';
473                if ( is_array( $fields ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
474                    $selector = '';
475
476                    // always needs id, so add if not present
477                    if ( ! in_array( 'ID', $fields ) ) {
478                        $selector = 'invoice.ID';
479                    }
480
481                    foreach ( $fields as $f ) {
482                        if ( ! empty( $selector ) ) {
483                            $selector .= ',';
484                        }
485                        $selector .= 'invoice.' . $f;
486                    }
487                } elseif ( $onlyID ) {
488                    $selector = 'invoice.ID';
489                }
490
491                #} ============ / PRE-QUERY ===========
492
493                #} Build query
494                $query = 'SELECT ' . $selector . $extraSelect . ' FROM ' . $ZBSCRM_t['invoices'] . ' as invoice';
495                #} ============= WHERE ================
496
497                if ( ! empty( $id ) && $id > 0 ) {
498
499                    #} Add ID
500                    $wheres['ID'] = array( 'ID', '=', '%d', $id );
501
502                }
503
504                if ( ! empty( $idOverride ) && $idOverride > 0 ) {
505
506                    #} Add idOverride
507                    $wheres['idOverride'] = array( 'zbsi_id_override', '=', '%d', $idOverride );
508
509                }
510
511                /*
512                3.0.13 WH removed - individual getInvoice should not have searchPhrase.
513                #} Add Search phrase
514                if (!empty($searchPhrase)){
515
516                    // search? - ALL THESE COLS should probs have index of FULLTEXT in db?
517                    $searchWheres = array();
518                    $searchWheres['search_ref'] = array('zbsi_id_override','LIKE','%s','%'.$searchPhrase.'%');
519                    $searchWheres['search_total'] = array('zbsi_total','LIKE','%s',$searchPhrase.'%');
520
521                    // 3.0.13 - Added ability to search custom fields (optionally)
522                    $customFieldSearch = zeroBSCRM_getSetting('customfieldsearch');
523                    if ($customFieldSearch == 1){
524
525                        // simplistic add
526                        // NOTE: This IGNORES ownership of custom field lines.
527                        $searchWheres['search_customfields'] = array('ID','IN',"(SELECT zbscf_objid FROM ".$ZBSCRM_t['customfields']." WHERE zbscf_objval LIKE %s AND zbscf_objtype = ".ZBS_TYPE_INVOICE.")",'%'.$searchPhrase.'%');
528
529                    }
530
531                    // This generates a query like 'zbsf_fname LIKE %s OR zbsf_lname LIKE %s',
532                    // which we then need to include as direct subquery (below) in main query :)
533                    $searchQueryArr = $this->buildWheres($searchWheres,'',array(),'OR',false);
534
535                    if (is_array($searchQueryArr) && isset($searchQueryArr['where']) && !empty($searchQueryArr['where'])){
536
537                        // add it
538                        $wheres['direct'][] = array('('.$searchQueryArr['where'].')',$searchQueryArr['params']);
539
540                    }
541
542                } */
543
544                if ( ! empty( $hash ) ) {
545
546                    #} Add hash
547                    $wheres['hash'] = array( 'zbsi_hash', '=', '%s', $hash );
548
549                }
550
551                if ( ! empty( $externalSource ) && ! empty( $externalSourceUID ) ) {
552
553                    $wheres['extsourcecheck'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_INVOICE . ' AND zbss_source = %s AND zbss_uid = %s)', array( $externalSource, $externalSourceUID ) );
554
555                }
556
557                #} ============ / WHERE ==============
558
559                #} Build out any WHERE clauses
560                $wheresArr = $this->buildWheres( $wheres, $whereStr, $params );
561                $whereStr  = $wheresArr['where'];
562                $params    = $params + $wheresArr['params'];
563                #} / Build WHERE
564
565                #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
566                $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
567                $ownQ   = $this->ownershipSQL( $ignoreowner );
568                if ( ! empty( $ownQ ) ) {
569                    $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
570                }
571                #} / Ownership
572
573                #} Append to sql (this also automatically deals with sortby and paging)
574                $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( 'ID', 'DESC' ) . $this->buildPaging( 0, 1 );
575
576                try {
577
578                    #} Prep & run query
579                    $queryObj     = $this->prepare( $query, $params );
580                    $potentialRes = $wpdb->get_row( $queryObj, OBJECT );
581
582                } catch ( Exception $e ) {
583
584                    #} General SQL Err
585                    $this->catchSQLError( $e );
586
587                }
588
589                #} Interpret Results (ROW)
590                if ( isset( $potentialRes ) && isset( $potentialRes->ID ) ) {
591
592                    #} Has results, tidy + return
593
594                        #} Only ID? return it directly
595                    if ( $onlyID ) {
596                        return $potentialRes->ID;
597                    }
598
599                    // tidy
600                    if ( is_array( $fields ) ) {
601                        // guesses fields based on table col names
602                        $res = $this->lazyTidyGeneric( $potentialRes );
603                    } else {
604                        // proper tidy
605                        $res = $this->tidy_invoice( $potentialRes, $withCustomFields );
606                    }
607
608                    if ( $withLineItems ) {
609
610                        // add all line item lines
611                        $res['lineitems'] = $this->DAL()->lineitems->getLineitems(
612                            array(
613                                'associatedObjType' => ZBS_TYPE_INVOICE,
614                                'associatedObjID'   => $potentialRes->ID,
615                                'perPage'           => 1000,
616                                'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
617                            )
618                        );
619
620                    }
621
622                    if ( $withTransactions ) {
623
624                        // add all transaction item lines
625                        $res['transactions'] = $this->DAL()->transactions->getTransactions(
626                            array(
627                                'assignedInvoice' => $potentialRes->ID,
628                                'perPage'         => 1000,
629                                'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TRANSACTION ),
630                            )
631                        );
632
633                    }
634
635                    if ( $withAssigned ) {
636
637                        /*
638                            This is for MULTIPLE (e.g. multi contact/companies assigned to an inv)
639
640                            // add all assigned contacts/companies
641                            $res['contacts'] = $this->DAL()->contacts->getContacts(array(
642                        'hasObjTypeLinkedTo'=>ZBS_TYPE_INVOICE,
643                        'hasObjIDLinkedTo'=>$resDataLine->ID,
644                        'perPage'=>-1,
645                        'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_CONTACT)));
646
647                            $res['companies'] = $this->DAL()->companies->getCompanies(array(
648                        'hasObjTypeLinkedTo'=>ZBS_TYPE_INVOICE,
649                        'hasObjIDLinkedTo'=>$resDataLine->ID,
650                        'perPage'=>-1,
651                        'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_COMPANY)));
652
653                        .. but we use 1:1, at least now: */
654
655                            // add all assigned contacts/companies
656                            $res['contact'] = $this->DAL()->contacts->getContacts(
657                                array(
658                                    'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
659                                    'hasObjIDLinkedTo'   => $potentialRes->ID,
660                                    'page'               => 0,
661                                    'perPage'            => 1, // FORCES 1
662                                    'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
663                                )
664                            );
665
666                            $res['company'] = $this->DAL()->companies->getCompanies(
667                                array(
668                                    'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
669                                    'hasObjIDLinkedTo'   => $potentialRes->ID,
670                                    'page'               => 0,
671                                    'perPage'            => 1, // FORCES 1
672                                    'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_COMPANY ),
673                                )
674                            );
675
676                    }
677
678                    if ( $withTags ) {
679
680                        // add all tags lines
681                        $res['tags'] = $this->DAL()->getTagsForObjID(
682                            array(
683                                'objtypeid' => ZBS_TYPE_INVOICE,
684                                'objid'     => $potentialRes->ID,
685                            )
686                        );
687
688                    }
689
690                    if ( $withFiles ) {
691
692                        $res['files'] = zeroBSCRM_files_getFiles( 'invoice', $potentialRes->ID );
693
694                    }
695
696                    if ( $withTotals ) {
697
698                        // add all tags lines
699                        $res['totals'] = $this->generateTotalsTable( $res );
700
701                    }
702
703                    return $res;
704
705                }
706            } // / if ID
707
708            return false;
709        }
710
711        /**
712         * returns invoice detail lines
713         *
714         * @param array $args Associative array of arguments
715         *
716         * @return array of invoice lines
717         */
718        public function getInvoices( $args = array() ) {
719
720            global $zbs;
721
722            #} ============ LOAD ARGS =============
723            $defaultArgs = array(
724
725                // Search/Filtering (leave as false to ignore)
726                'searchPhrase'     => '', // searches id_override (ref) (not lineitems yet)
727                'inArr'            => false,
728                'isTagged'         => false, // 1x INT OR array(1,2,3)
729                'isNotTagged'      => false, // 1x INT OR array(1,2,3)
730                'ownedBy'          => false,
731                'externalSource'   => false, // e.g. paypal
732                'olderThan'        => false, // uts
733                'newerThan'        => false, // uts
734                'hasStatus'        => false, // Lead (this takes over from the quick filter post 19/6/18)
735                'otherStatus'      => false, // status other than 'Lead'
736                'assignedContact'  => false, // assigned to contact id (int)
737                'assignedCompany'  => false, // assigned to company id (int)
738                'quickFilters'     => false, // booo
739
740            // returns
741                'count'            => false,
742                'withLineItems'    => true,
743                'withCustomFields' => true,
744                'withTransactions' => false, // gets trans associated with inv as well
745                'withTags'         => false,
746                'withOwner'        => false,
747                'withAssigned'     => false, // return ['contact'] & ['company'] objs if has link
748                'withFiles'        => false,
749                '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)
750                'withTotals'       => false, // uses $this->generateTotalsTable to also calc discount + taxes on fly
751
752                'sortByField'      => 'ID',
753                'sortOrder'        => 'ASC',
754                'page'             => 0, // this is what page it is (gets * by for limit)
755                'perPage'          => 100,
756                'whereCase'        => 'AND', // DEFAULT = AND
757
758            // permissions
759                'ignoreowner'      => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_INVOICE ), // this'll let you not-check the owner of obj
760
761            );
762            foreach ( $defaultArgs as $argK => $argV ) {
763                $$argK = $argV;
764                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
765                    if ( is_array( $args[ $argK ] ) ) {
766                        $newData = $$argK;
767                        if ( ! is_array( $newData ) ) {
768                            $newData = array();
769                        } foreach ( $args[ $argK ] as $subK => $subV ) {
770                            $newData[ $subK ] = $subV;
771                        }$$argK = $newData;
772                    } else {
773                        $$argK = $args[ $argK ]; }
774                }
775            }
776            #} =========== / LOAD ARGS =============
777
778            global $ZBSCRM_t, $wpdb, $zbs;
779            $wheres          = array( 'direct' => array() );
780            $whereStr        = '';
781            $additionalWhere = '';
782            $params          = array();
783            $res             = array();
784            $joinQ           = '';
785            $extraSelect     = '';
786
787            #} ============= PRE-QUERY ============
788
789            #} Capitalise this
790            $sortOrder = strtoupper( $sortOrder );
791
792            #} If just count, turn off any extra gumpf
793            if ( $count ) {
794                $withCustomFields = false;
795                $withTags         = false;
796                $withTransactions = false;
797                $withOwner        = false;
798                $withAssigned     = false;
799            }
800
801            #} If onlyColumns, validate
802            if ( $onlyColumns ) {
803
804                #} onlyColumns build out a field arr
805                if ( is_array( $onlyColumns ) && count( $onlyColumns ) > 0 ) {
806
807                    $onlyColumnsFieldArr = array();
808                    foreach ( $onlyColumns as $col ) {
809
810                        // find db col key from field key (e.g. fname => zbsc_fname)
811                        $dbCol = '';
812                        if ( isset( $this->objectModel[ $col ] ) && isset( $this->objectModel[ $col ]['fieldname'] ) ) {
813                            $dbCol = $this->objectModel[ $col ]['fieldname'];
814                        }
815
816                        if ( ! empty( $dbCol ) ) {
817
818                            $onlyColumnsFieldArr[ $dbCol ] = $col;
819
820                        }
821                    }
822                }
823
824                // if legit cols:
825                if ( isset( $onlyColumnsFieldArr ) && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
826
827                    $onlyColumns = true;
828
829                    // If onlyColumns, turn off extras
830                    $withCustomFields = false;
831                    $withTags         = false;
832                    $withTransactions = false;
833                    $withOwner        = false;
834                    $withAssigned     = false;
835                    $withTotals       = false;
836
837                } else {
838
839                    // deny
840                    $onlyColumns = false;
841
842                }
843            }
844
845            #} Custom Fields
846                        // @phan-suppress-next-line PhanImpossibleCondition -- Phan is confused; this var is initialized at the beginning of the function.
847            if ( $withCustomFields ) {
848
849                #} Retrieve any cf
850                $custFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_INVOICE ) );
851
852                #} Cycle through + build into query
853                if ( is_array( $custFields ) ) {
854                    foreach ( $custFields as $cK => $cF ) {
855
856                        // custom field (e.g. 'third name') it'll be passed here as 'third-name'
857                        // ... problem is mysql does not like that :) so we have to chage here:
858                        // in this case we prepend cf's with cf_ and we switch - for _
859                        $cKey = 'cf_' . str_replace( '-', '_', $cK );
860
861                        // we also check the $sortByField in case that's the same cf
862                        if ( $cK == $sortByField ) {
863
864                            // sort by
865                            $sortByField = $cKey;
866
867                            // check if sort needs any CAST (e.g. numeric):
868                            $sortByField = $this->DAL()->build_custom_field_order_by_str( $sortByField, $cF );
869
870                        }
871
872                        // add as subquery
873                        $extraSelect .= ',(SELECT zbscf_objval FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objid = invoice.ID AND zbscf_objkey = %s AND zbscf_objtype = %d LIMIT 1) ' . $cKey;
874
875                        // add params
876                        $params[] = $cK;
877                        $params[] = ZBS_TYPE_INVOICE;
878
879                    }
880                }
881            }
882
883            #} ============ / PRE-QUERY ===========
884
885            #} Build query
886            $query = 'SELECT invoice.*' . $extraSelect . ' FROM ' . $ZBSCRM_t['invoices'] . ' as invoice' . $joinQ;
887
888            #} Count override
889            if ( $count ) {
890                $query = 'SELECT COUNT(invoice.ID) FROM ' . $ZBSCRM_t['invoices'] . ' as invoice' . $joinQ;
891            }
892
893            #} onlyColumns override
894            if ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
895
896                $columnStr = '';
897                foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
898
899                    if ( ! empty( $columnStr ) ) {
900                        $columnStr .= ',';
901                    }
902                    // this presumes str is db-safe? could do with sanitation?
903                    $columnStr .= $colDBKey;
904
905                }
906
907                $query = 'SELECT ' . $columnStr . ' FROM ' . $ZBSCRM_t['invoices'] . ' as invoice' . $joinQ;
908
909            }
910
911            #} ============= WHERE ================
912
913            #} Add Search phrase
914            if ( ! empty( $searchPhrase ) ) {
915
916                // search? - ALL THESE COLS should probs have index of FULLTEXT in db?
917                $searchWheres                 = array();
918                $searchWheres['search_ID']    = array( 'ID', '=', '%d', $searchPhrase );
919                $searchWheres['search_ref']   = array( 'zbsi_id_override', 'LIKE', '%s', '%' . $searchPhrase . '%' );
920                $searchWheres['search_total'] = array( 'zbsi_total', 'LIKE', '%s', $searchPhrase . '%' );
921
922                // 3.0.13 - Added ability to search custom fields (optionally)
923                $customFieldSearch = zeroBSCRM_getSetting( 'customfieldsearch' );
924                if ( $customFieldSearch == 1 ) {
925
926                    // simplistic add
927                    // NOTE: This IGNORES ownership of custom field lines.
928                    $searchWheres['search_customfields'] = array( 'ID', 'IN', '(SELECT zbscf_objid FROM ' . $ZBSCRM_t['customfields'] . ' WHERE zbscf_objval LIKE %s AND zbscf_objtype = ' . ZBS_TYPE_INVOICE . ')', '%' . $searchPhrase . '%' );
929
930                }
931
932                // This generates a query like 'zbsi_fname LIKE %s OR zbsi_lname LIKE %s',
933                // which we then need to include as direct subquery (below) in main query :)
934                $searchQueryArr = $this->buildWheres( $searchWheres, '', array(), 'OR', false );
935
936                if ( is_array( $searchQueryArr ) && isset( $searchQueryArr['where'] ) && ! empty( $searchQueryArr['where'] ) ) {
937
938                    // add it
939                    $wheres['direct'][] = array( '(' . $searchQueryArr['where'] . ')', $searchQueryArr['params'] );
940
941                }
942            }
943
944            #} In array (if inCompany passed, this'll currently overwrite that?! (todo2.5))
945            if ( is_array( $inArr ) && count( $inArr ) > 0 ) {
946
947                // clean for ints
948                $inArrChecked = array();
949                foreach ( $inArr as $x ) {
950                    $inArrChecked[] = (int) $x; }
951
952                // add where
953                $wheres['inarray'] = array( 'ID', 'IN', '(' . implode( ',', $inArrChecked ) . ')' );
954
955            }
956
957            #} Owned by
958            if ( ! empty( $ownedBy ) && $ownedBy > 0 ) {
959
960                // would never hard-type this in (would make generic as in buildWPMetaQueryWhere)
961                // but this is only here until MIGRATED to db2 globally
962                // $wheres['incompany'] = array('ID','IN','(SELECT DISTINCT post_id FROM '.$wpdb->prefix."postmeta WHERE meta_key = 'zbs_company' AND meta_value = %d)",$inCompany);
963                // Use obj links now
964                $wheres['ownedBy'] = array( 'zbs_owner', '=', '%s', $ownedBy );
965
966            }
967
968            // External sources
969            if ( ! empty( $externalSource ) ) {
970
971                // NO owernship built into this, check when roll out multi-layered ownsership
972                $wheres['externalsource'] = array( 'ID', 'IN', '(SELECT DISTINCT zbss_objid FROM ' . $ZBSCRM_t['externalsources'] . ' WHERE zbss_objtype = ' . ZBS_TYPE_INVOICE . ' AND zbss_source = %s)', $externalSource );
973
974            }
975
976            // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
977            // quick addition for mike
978            #} olderThan
979            if ( ! empty( $olderThan ) && $olderThan > 0 ) {
980                $wheres['olderThan'] = array( 'zbsi_created', '<=', '%d', $olderThan );
981            }
982            #} newerThan
983            if ( ! empty( $newerThan ) && $newerThan > 0 ) {
984                $wheres['newerThan'] = array( 'zbsi_created', '>=', '%d', $newerThan );
985            }
986
987            // status
988            if ( ! empty( $hasStatus ) ) {
989                $wheres['hasStatus'] = array( 'zbsi_status', '=', '%s', $hasStatus );
990            }
991            if ( ! empty( $otherStatus ) ) {
992                $wheres['otherStatus'] = array( 'zbsi_status', '<>', '%s', $otherStatus );
993            }
994
995            // assignedContact + assignedCompany
996            if ( ! empty( $assignedContact ) && $assignedContact > 0 ) {
997                $wheres['assignedContact'] = array( 'ID', 'IN', '(SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_to = %d)', $assignedContact );
998            }
999            if ( ! empty( $assignedCompany ) && $assignedCompany > 0 ) {
1000                $wheres['assignedCompany'] = array( 'ID', 'IN', '(SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_COMPANY . ' AND zbsol_objid_to = %d)', $assignedCompany );
1001            }
1002            // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1003
1004
1005            #} Quick filters - adapted from DAL1 (probs can be slicker)
1006            if ( is_array( $quickFilters ) && count( $quickFilters ) > 0 ) {
1007
1008                // cycle through
1009                foreach ( $quickFilters as $qFilter ) {
1010
1011                    // where status = x
1012                    // USE hasStatus above now...
1013                    if ( str_starts_with( $qFilter, 'status_' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1014
1015                        $quick_filter_status         = substr( $qFilter, 7 ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1016                        $wheres['quickfilterstatus'] = array( 'zbsi_status', '=', 'convert(%s using utf8mb4) collate utf8mb4_bin', $quick_filter_status );
1017
1018                    } else {
1019
1020                        // if we've hit no filter query, let external logic hook in to provide alternatives
1021                        // First used in WooSync module
1022                        $wheres = apply_filters( 'jpcrm_invoice_query_quickfilter', $wheres, $qFilter );
1023
1024                    }
1025                }
1026            } // / quickfilters
1027
1028            #} Is Tagged (expects 1 tag ID OR array)
1029
1030                // catch 1 item arr
1031            if ( is_array( $isTagged ) && count( $isTagged ) == 1 ) {
1032                $isTagged = $isTagged[0];
1033            }
1034
1035            if ( ! empty( $isTagged ) && ! is_array( $isTagged ) && $isTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1036
1037                // add where tagged
1038                // 1 int:
1039                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = invoice.ID AND zbstl_tagid = %d) > 0)', array( ZBS_TYPE_INVOICE, $isTagged ) );
1040
1041            } elseif ( is_array( $isTagged ) && count( $isTagged ) > 0 ) {
1042
1043                // foreach in array :)
1044                $tagStr = '';
1045                foreach ( $isTagged as $iTag ) {
1046                    $i = (int) $iTag;
1047                    if ( $i > 0 ) {
1048
1049                        if ( $tagStr !== '' ) {
1050                            $tagStr . ',';
1051                        }
1052                        $tagStr .= $i;
1053                    }
1054                }
1055                if ( ! empty( $tagStr ) ) {
1056
1057                    $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = invoice.ID AND zbstl_tagid IN (%s)) > 0)', array( ZBS_TYPE_INVOICE, $tagStr ) );
1058
1059                }
1060            }
1061            #} Is NOT Tagged (expects 1 tag ID OR array)
1062
1063                // catch 1 item arr
1064            if ( is_array( $isNotTagged ) && count( $isNotTagged ) == 1 ) {
1065                $isNotTagged = $isNotTagged[0];
1066            }
1067
1068            if ( ! empty( $isNotTagged ) && ! is_array( $isNotTagged ) && $isNotTagged > 0 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1069
1070                // add where tagged
1071                // 1 int:
1072                $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = invoice.ID AND zbstl_tagid = %d) = 0)', array( ZBS_TYPE_INVOICE, $isNotTagged ) );
1073
1074            } elseif ( is_array( $isNotTagged ) && count( $isNotTagged ) > 0 ) {
1075
1076                // foreach in array :)
1077                $tagStr = '';
1078                foreach ( $isNotTagged as $iTag ) {
1079                    $i = (int) $iTag;
1080                    if ( $i > 0 ) {
1081
1082                        if ( $tagStr !== '' ) {
1083                            $tagStr . ',';
1084                        }
1085                        $tagStr .= $i;
1086                    }
1087                }
1088                if ( ! empty( $tagStr ) ) {
1089
1090                    $wheres['direct'][] = array( '((SELECT COUNT(ID) FROM ' . $ZBSCRM_t['taglinks'] . ' WHERE zbstl_objtype = %d AND zbstl_objid = invoice.ID AND zbstl_tagid IN (%s)) = 0)', array( ZBS_TYPE_INVOICE, $tagStr ) );
1091
1092                }
1093            }
1094
1095            #} ============ / WHERE ===============
1096
1097            #} ============   SORT   ==============
1098
1099            // Obj Model based sort conversion
1100            // converts 'addr1' => 'zbsco_addr1' generically
1101            if ( isset( $this->objectModel[ $sortByField ] ) && isset( $this->objectModel[ $sortByField ]['fieldname'] ) ) {
1102                $sortByField = $this->objectModel[ $sortByField ]['fieldname'];
1103            }
1104
1105            // Mapped sorts
1106            // This catches listview and other exception sort cases
1107            $sort_map = array(
1108
1109                // field aliases
1110                'ref'      => 'zbsi_id_override',
1111                'value'    => 'zbsi_total',
1112
1113                // Note: "customer" here could be company or contact, so it's not a true sort (as no great way of doing this beyond some sort of prefix comparing)
1114                'customer' => '(SELECT ID FROM ' . $ZBSCRM_t['contacts'] . ' WHERE ID IN (SELECT zbsol_objid_to FROM ' . $ZBSCRM_t['objlinks'] . ' WHERE zbsol_objtype_from = ' . ZBS_TYPE_INVOICE . ' AND zbsol_objtype_to = ' . ZBS_TYPE_CONTACT . ' AND zbsol_objid_from = invoice.ID))',
1115
1116            );
1117
1118            if ( array_key_exists( $sortByField, $sort_map ) ) {
1119
1120                $sortByField = $sort_map[ $sortByField ];
1121
1122            }
1123
1124            #} ============ / SORT   ==============
1125
1126            #} CHECK this + reset to default if faulty
1127            if ( ! in_array( $whereCase, array( 'AND', 'OR' ) ) ) {
1128                $whereCase = 'AND';
1129            }
1130
1131            #} Build out any WHERE clauses
1132            $wheresArr = $this->buildWheres( $wheres, $whereStr, $params, $whereCase );
1133            $whereStr  = $wheresArr['where'];
1134            $params    = $params + $wheresArr['params'];
1135            #} / Build WHERE
1136
1137            #} Ownership v1.0 - the following adds SITE + TEAM checks, and (optionally), owner
1138            $params = array_merge( $params, $this->ownershipQueryVars( $ignoreowner ) ); // merges in any req.
1139            $ownQ   = $this->ownershipSQL( $ignoreowner, 'contact' );
1140            if ( ! empty( $ownQ ) ) {
1141                $additionalWhere = $this->spaceAnd( $additionalWhere ) . $ownQ; // adds str to query
1142            }
1143            #} / Ownership
1144
1145            #} Append to sql (this also automatically deals with sortby and paging)
1146            $query .= $this->buildWhereStr( $whereStr, $additionalWhere ) . $this->buildSort( $sortByField, $sortOrder ) . $this->buildPaging( $page, $perPage );
1147
1148            try {
1149
1150                #} Prep & run query
1151                $queryObj = $this->prepare( $query, $params );
1152
1153                #} Catch count + return if requested
1154                if ( $count ) {
1155                    return $wpdb->get_var( $queryObj );
1156                }
1157
1158                #} else continue..
1159                $potentialRes = $wpdb->get_results( $queryObj, OBJECT );
1160
1161            } catch ( Exception $e ) {
1162
1163                #} General SQL Err
1164                $this->catchSQLError( $e );
1165
1166            }
1167
1168            #} Interpret results (Result Set - multi-row)
1169            if ( isset( $potentialRes ) && is_array( $potentialRes ) && count( $potentialRes ) > 0 ) {
1170
1171                #} Has results, tidy + return
1172                foreach ( $potentialRes as $resDataLine ) {
1173
1174                    // using onlyColumns filter?
1175                    if ( $onlyColumns && is_array( $onlyColumnsFieldArr ) && count( $onlyColumnsFieldArr ) > 0 ) {
1176
1177                        // only coumns return.
1178                        $resArr = array();
1179                        foreach ( $onlyColumnsFieldArr as $colDBKey => $colStr ) {
1180
1181                            if ( isset( $resDataLine->$colDBKey ) ) {
1182                                $resArr[ $colStr ] = $resDataLine->$colDBKey;
1183                            }
1184                        }
1185                    } else {
1186
1187                        // tidy
1188                        $resArr = $this->tidy_invoice( $resDataLine, $withCustomFields );
1189
1190                    }
1191
1192                    if ( $withLineItems ) {
1193
1194                        // add all line item lines
1195                        $resArr['lineitems'] = $this->DAL()->lineitems->getLineitems(
1196                            array(
1197                                'associatedObjType' => ZBS_TYPE_INVOICE,
1198                                'associatedObjID'   => $resDataLine->ID,
1199                                'perPage'           => 1000,
1200                                'ignoreowner'       => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_LINEITEM ),
1201                            )
1202                        );
1203
1204                    }
1205
1206                    if ( $withTransactions ) {
1207
1208                        // add all line item lines
1209                        $resArr['transactions'] = $this->DAL()->transactions->getTransactions(
1210                            array(
1211                                'assignedInvoice' => $resDataLine->ID,
1212                                'perPage'         => 1000,
1213                                'ignoreowner'     => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_TRANSACTION ),
1214                            )
1215                        );
1216
1217                    }
1218
1219                    if ( $withTags ) {
1220
1221                        // add all tags lines
1222                        $resArr['tags'] = $this->DAL()->getTagsForObjID(
1223                            array(
1224                                'objtypeid' => ZBS_TYPE_INVOICE,
1225                                'objid'     => $resDataLine->ID,
1226                            )
1227                        );
1228
1229                    }
1230
1231                    if ( $withOwner ) {
1232
1233                        $resArr['owner'] = zeroBS_getOwner( $resDataLine->ID, true, ZBS_TYPE_INVOICE, $resDataLine->zbs_owner );
1234
1235                    }
1236
1237                    if ( $withAssigned ) {
1238
1239                        /*
1240                        This is for MULTIPLE (e.g. multi contact/companies assigned to an inv)
1241
1242                        // add all assigned contacts/companies
1243                        $res['contacts'] = $this->DAL()->contacts->getContacts(array(
1244                            'hasObjTypeLinkedTo'=>ZBS_TYPE_INVOICE,
1245                            'hasObjIDLinkedTo'=>$resDataLine->ID,
1246                            'perPage'=>-1,
1247                            'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_CONTACT)));
1248
1249                        $res['companies'] = $this->DAL()->companies->getCompanies(array(
1250                            'hasObjTypeLinkedTo'=>ZBS_TYPE_INVOICE,
1251                            'hasObjIDLinkedTo'=>$resDataLine->ID,
1252                            'perPage'=>-1,
1253                            'ignoreowner'=>zeroBSCRM_DAL2_ignoreOwnership(ZBS_TYPE_COMPANY)));
1254
1255                        .. but we use 1:1, at least now: */
1256
1257                        // add all assigned contacts/companies
1258                        $resArr['contact'] = $this->DAL()->contacts->getContacts(
1259                            array(
1260                                'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
1261                                'hasObjIDLinkedTo'   => $resDataLine->ID,
1262                                'page'               => 0,
1263                                'perPage'            => 1, // FORCES 1
1264                                'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
1265                            )
1266                        );
1267
1268                            $resArr['company'] = $this->DAL()->companies->getCompanies(
1269                                array(
1270                                    'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
1271                                    'hasObjIDLinkedTo'   => $resDataLine->ID,
1272                                    'page'               => 0,
1273                                    'perPage'            => 1, // FORCES 1
1274                                    'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_COMPANY ),
1275                                )
1276                            );
1277
1278                    }
1279
1280                    if ( $withFiles ) {
1281
1282                        $resArr['files'] = zeroBSCRM_files_getFiles( 'invoice', $resDataLine->ID );
1283
1284                    }
1285
1286                    if ( $withTotals ) {
1287
1288                        // add all tags lines
1289                        $resArr['totals'] = $this->generateTotalsTable( $resArr );
1290
1291                    }
1292
1293                    $res[] = $resArr;
1294
1295                }
1296            }
1297
1298            return $res;
1299        }
1300
1301        /**
1302         * Returns a count of invoices (owned)
1303         * .. inc by status
1304         *
1305         * @return int count
1306         */
1307        public function getInvoiceCount( $args = array() ) {
1308
1309            #} ============ LOAD ARGS =============
1310            $defaultArgs = array(
1311
1312                // Search/Filtering (leave as false to ignore)
1313                'withStatus'  => false, // will be str if used
1314
1315            // permissions
1316                'ignoreowner' => true, // this'll let you not-check the owner of obj
1317
1318            );
1319            foreach ( $defaultArgs as $argK => $argV ) {
1320                $$argK = $argV;
1321                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1322                    if ( is_array( $args[ $argK ] ) ) {
1323                        $newData = $$argK;
1324                        if ( ! is_array( $newData ) ) {
1325                            $newData = array();
1326                        } foreach ( $args[ $argK ] as $subK => $subV ) {
1327                            $newData[ $subK ] = $subV;
1328                        }$$argK = $newData;
1329                    } else {
1330                        $$argK = $args[ $argK ]; }
1331                }
1332            }
1333            #} =========== / LOAD ARGS =============
1334
1335            $whereArr = array();
1336
1337            if ( $withStatus !== false && ! empty( $withStatus ) ) {
1338                $whereArr['status'] = array( 'zbsi_status', '=', '%s', $withStatus );
1339            }
1340
1341            return $this->DAL()->getFieldByWHERE(
1342                array(
1343                    'objtype'     => ZBS_TYPE_INVOICE,
1344                    'colname'     => 'COUNT(ID)',
1345                    'where'       => $whereArr,
1346                    'ignoreowner' => $ignoreowner,
1347                )
1348            );
1349        }
1350
1351        /**
1352         * adds or updates a invoice object
1353         *
1354         * @param array $args Associative array of arguments
1355         *              id (if update), owner, data (array of field data)
1356         *
1357         * @return int line ID
1358         */
1359        public function addUpdateInvoice( $args = array() ) {
1360
1361            global $ZBSCRM_t, $wpdb, $zbs;
1362
1363            #} Retrieve any cf
1364            $customFields = $this->DAL()->getActiveCustomFields( array( 'objtypeid' => ZBS_TYPE_INVOICE ) );
1365            // not req. here$addrCustomFields = $this->DAL()->getActiveCustomFields(array('objtypeid'=>ZBS_TYPE_ADDRESS));
1366
1367            #} ============ LOAD ARGS =============
1368            $defaultArgs = array(
1369
1370                'id'                   => -1,
1371                'owner'                => -1,
1372
1373                // fields (directly)
1374                'data'                 => array(
1375
1376                    'id_override'         => '',
1377                    'parent'              => '',
1378                    'status'              => '',
1379                    'hash'                => '',
1380                    'pdf_template'        => '',
1381                    'portal_template'     => '',
1382                    'email_template'      => '',
1383                    'invoice_frequency'   => '',
1384                    'currency'            => '',
1385                    'pay_via'             => '',
1386                    'logo_url'            => '',
1387                    'address_to_objtype'  => '',
1388                    'addressed_from'      => '',
1389                    'addressed_to'        => '',
1390                    'allow_partial'       => -1,
1391                    'allow_tip'           => -1,
1392                    'send_attachments'    => -1,
1393                    'hours_or_quantity'   => '',
1394                    'date'                => '',
1395                    'due_date'            => '',
1396                    'paid_date'           => '',
1397                    'hash_viewed'         => '',
1398                    'hash_viewed_count'   => '',
1399                    'portal_viewed'       => '',
1400                    'portal_viewed_count' => '',
1401                    'net'                 => '',
1402                    'discount'            => '',
1403                    'discount_type'       => '',
1404                    'shipping'            => '',
1405                    'shipping_taxes'      => '',
1406                    'shipping_tax'        => '',
1407                    'taxes'               => '',
1408                    'tax'                 => '',
1409                    'total'               => '',
1410
1411                    // lineitems:
1412                    'lineitems'           => false,
1413                    // will be an array of lineitem lines (as per matching lineitem database model)
1414                    // note:    if no change desired, pass "false"
1415                    // if removal of all/change, pass empty array
1416
1417                    // obj links:
1418                    'contacts'            => false, // array of id's
1419                    'companies'           => false, // array of id's
1420
1421                    // Note Custom fields may be passed here, but will not have defaults so check isset()
1422
1423                    // tags
1424                    'tags'                => -1, // pass an array of tag ids or tag strings
1425                    'tag_mode'            => 'replace', // replace|append|remove
1426
1427                    'externalSources'     => -1, // if this is an array(array('source'=>src,'uid'=>uid),multiple()) it'll add :)
1428
1429                    'created'             => -1,
1430                    'lastupdated'         => '',
1431
1432                ),
1433
1434                'limitedFields'        => -1, // if this is set it OVERRIDES data (allowing you to set specific fields + leave rest in tact)
1435            // ^^ will look like: array(array('key'=>x,'val'=>y,'type'=>'%s')). the key needs to match the DB table, i.e. zbsi_status and not
1436            // just status. For full key references see developer docs (link to follow).
1437
1438            // this function as DAL1 func did.
1439                'extraMeta'            => -1,
1440                'automatorPassthrough' => -1,
1441
1442                'silentInsert'         => false, // this was for init Migration - it KILLS all IA for newInvoice (because is migrating, not creating new :) this was -1 before
1443
1444                'do_not_update_blanks' => false, // this allows you to not update fields if blank (same as fieldoverride for extsource -> in)
1445
1446                'calculate_totals'     => false, // This allows us to recalculate tax, subtotal, total via php (e.g. if added via api). Only works if not using limitedFields
1447
1448            );
1449            foreach ( $defaultArgs as $argK => $argV ) {
1450                $$argK = $argV;
1451                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
1452                    if ( is_array( $args[ $argK ] ) ) {
1453                        $newData = $$argK;
1454                        if ( ! is_array( $newData ) ) {
1455                            $newData = array();
1456                        } foreach ( $args[ $argK ] as $subK => $subV ) {
1457                            $newData[ $subK ] = $subV;
1458                        }$$argK = $newData;
1459                    } else {
1460                        $$argK = $args[ $argK ]; }
1461                }
1462            }
1463
1464            // Needs this to grab custom fields (if passed) too :)
1465            if ( is_array( $customFields ) ) {
1466                foreach ( $customFields as $cK => $cF ) {
1467                    // only for data, limited fields below
1468                    if ( is_array( $data ) ) {
1469                        if ( isset( $args['data'][ $cK ] ) ) {
1470                            $data[ $cK ] = $args['data'][ $cK ];
1471                        }
1472                    }
1473                }
1474            }
1475
1476            // this takes limited fields + checks through for custom fields present
1477            // (either as key zbsi_source or source, for example)
1478            // then switches them into the $data array, for separate update
1479            // where this'll fall over is if NO normal contact data is sent to update, just custom fields
1480            if ( is_array( $limitedFields ) && is_array( $customFields ) ) {
1481
1482                    // $customFieldKeys = array_keys($customFields);
1483                    $newLimitedFields = array();
1484
1485                    // cycle through
1486                foreach ( $limitedFields as $field ) {
1487
1488                    // some weird case where getting empties, so added check
1489                    if ( isset( $field['key'] ) && ! empty( $field['key'] ) ) {
1490
1491                        $dePrefixed = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1492                        if ( str_starts_with( $field['key'], 'zbsi_' ) ) {
1493                            $dePrefixed = substr( $field['key'], strlen( 'zbsi_' ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1494                        }
1495
1496                        if ( isset( $customFields[ $field['key'] ] ) ) {
1497
1498                            // is custom, move to data
1499                            $data[ $field['key'] ] = $field['val'];
1500
1501                        } elseif ( ! empty( $dePrefixed ) && isset( $customFields[ $dePrefixed ] ) ) {
1502
1503                            // is custom, move to data
1504                            $data[ $dePrefixed ] = $field['val'];
1505
1506                        } else {
1507
1508                            // add it to limitedFields (it's not dealt with post-update)
1509                            $newLimitedFields[] = $field;
1510                        }
1511                    }
1512                }
1513
1514                    // move this back in
1515                    $limitedFields = $newLimitedFields;
1516                    unset( $newLimitedFields );
1517
1518            }
1519
1520            #} =========== / LOAD ARGS ============
1521
1522            #} ========== CHECK FIELDS ============
1523
1524            $id = (int) $id;
1525
1526            // here we check that the potential owner CAN even own
1527            if ( $owner > 0 && ! user_can( $owner, 'admin_zerobs_usr' ) ) {
1528                $owner = -1;
1529            }
1530
1531            // if owner = -1, add current
1532            if ( ! isset( $owner ) || $owner === -1 ) {
1533                $owner = zeroBSCRM_user(); }
1534
1535            if ( is_array( $limitedFields ) ) {
1536
1537                // LIMITED UPDATE (only a few fields.)
1538                if ( ! is_array( $limitedFields ) || count( $limitedFields ) <= 0 ) {
1539                    return false;
1540                }
1541                // REQ. ID too (can only update)
1542                if ( empty( $id ) || $id <= 0 ) {
1543                    return false;
1544                }
1545            } else {
1546
1547                // NORMAL, FULL UPDATE
1548
1549            }
1550
1551            #} If no status, and default is specified in settings, add that in :)
1552            if ( $data['status'] === null || ! isset( $data['status'] ) || empty( $data['status'] ) ) {
1553
1554                // Default status for obj? -> this one gets for contacts -> $zbsCustomerMeta['status'] = zeroBSCRM_getSetting('defaultstatus');
1555                // For now we force 'Draft'
1556                $data['status'] = __( 'Draft', 'zero-bs-crm' );
1557            }
1558
1559            #} ========= / CHECK FIELDS ===========
1560
1561            #} ========= OVERRIDE SETTING (Deny blank overrides) ===========
1562
1563            // this only functions if externalsource is set (e.g. api/form, etc.)
1564            if ( isset( $data['externalSources'] ) && is_array( $data['externalSources'] ) && count( $data['externalSources'] ) > 0 ) {
1565                if ( zeroBSCRM_getSetting( 'fieldoverride' ) == '1' ) {
1566
1567                    $do_not_update_blanks = true;
1568
1569                }
1570            }
1571
1572            // either ext source + setting, or set by the func call
1573            if ( $do_not_update_blanks ) {
1574
1575                // this setting says 'don't override filled-out data with blanks'
1576                // so here we check through any passed blanks + convert to limitedFields
1577                // only matters if $id is set (there is somt to update not add
1578                if ( isset( $id ) && ! empty( $id ) && $id > 0 ) {
1579
1580                    // get data to copy over (for now, this is required to remove 'fullname' etc.)
1581                    $dbData = $this->db_ready_invoice( $data );
1582                    // unset($dbData['id']); // this is unset because we use $id, and is update, so not req. legacy issue
1583                    // 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 :)
1584
1585                    $origData    = $data; // $data = array();
1586                    $limitedData = array(); // array(array('key'=>'zbsi_x','val'=>y,'type'=>'%s'))
1587
1588                    // cycle through + translate into limitedFields (removing any blanks, or arrays (e.g. externalSources))
1589                    // we also have to remake a 'faux' data (removing blanks for tags etc.) for the post-update updates
1590                    foreach ( $dbData as $k => $v ) {
1591
1592                        $intV = (int) $v;
1593
1594                        // only add if valuenot empty
1595                        if ( ! is_array( $v ) && ! empty( $v ) && $v != '' && $v !== 0 && $v !== -1 && $intV !== -1 ) {
1596
1597                            // add to update arr
1598                            $limitedData[] = array(
1599                                'key'  => 'zbsi_' . $k, // we have to add zbsi_ here because translating from data -> limited fields
1600                                'val'  => $v,
1601                                'type' => $this->getTypeStr( 'zbsi_' . $k ),
1602                            );
1603
1604                            // add to remade $data for post-update updates
1605                            $data[ $k ] = $v;
1606
1607                        }
1608                    }
1609
1610                    // copy over
1611                    $limitedFields = $limitedData;
1612
1613                } // / if ID
1614
1615            } // / if do_not_update_blanks
1616
1617            #} ========= / OVERRIDE SETTING (Deny blank overrides) ===========
1618
1619            #} ========= BUILD DATA ===========
1620
1621            $update  = false;
1622            $dataArr = array();
1623            $typeArr = array();
1624
1625            if ( is_array( $limitedFields ) ) {
1626
1627                // LIMITED FIELDS
1628                $update = true;
1629
1630                // cycle through
1631                foreach ( $limitedFields as $field ) {
1632
1633                    // some weird case where getting empties, so added check
1634                    if ( ! empty( $field['key'] ) ) {
1635                        $dataArr[ $field['key'] ] = $field['val'];
1636                        $typeArr[]                = $field['type'];
1637                    }
1638                }
1639
1640                // add update time
1641                if ( ! isset( $dataArr['zbsi_lastupdated'] ) ) {
1642                    $dataArr['zbsi_lastupdated'] = time();
1643                    $typeArr[]                   = '%d'; }
1644            } else {
1645
1646                        // FULL UPDATE/INSERT
1647
1648                // (re)calculate the totals etc?
1649                if ( $calculate_totals ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1650
1651                    $data = $this->recalculate( $data );
1652
1653                }
1654
1655                // contacts - avoid dupes
1656                if ( isset( $data['contacts'] ) && is_array( $data['contacts'] ) ) {
1657
1658                    $coArr = array();
1659                    foreach ( $data['contacts'] as $c ) {
1660                        $cI = (int) $c;
1661                        if ( $cI > 0 && ! in_array( $cI, $coArr ) ) {
1662                            $coArr[] = $cI;
1663                        }
1664                    }
1665
1666                    // reset the main
1667                    if ( count( $coArr ) > 0 ) {
1668                        $data['contacts'] = $coArr;
1669                    } else {
1670                        $data['contacts'] = 'unset';
1671                    }
1672                    unset( $coArr );
1673
1674                }
1675
1676                // companies - avoid dupes
1677                if ( isset( $data['companies'] ) && is_array( $data['companies'] ) ) {
1678
1679                    $coArr = array();
1680                    foreach ( $data['companies'] as $c ) {
1681                        $cI = (int) $c;
1682                        if ( $cI > 0 && ! in_array( $cI, $coArr ) ) {
1683                            $coArr[] = $cI;
1684                        }
1685                    }
1686
1687                    // reset the main
1688                    if ( count( $coArr ) > 0 ) {
1689                        $data['companies'] = $coArr;
1690                    } else {
1691                        $data['companies'] = 'unset';
1692                    }
1693                    unset( $coArr );
1694
1695                }
1696
1697                // UPDATE
1698                $dataArr = array(
1699
1700                    // ownership
1701                    // no need to update these (as of yet) - can't move teams etc.
1702                    // 'zbs_site' => zeroBSCRM_installSite(),
1703                    // 'zbs_team' => zeroBSCRM_installTeam(),
1704                    // 'zbs_owner' => $owner,
1705
1706                    'zbsi_id_override'         => $data['id_override'],
1707                    'zbsi_parent'              => $data['parent'],
1708                    'zbsi_status'              => $data['status'],
1709                    'zbsi_hash'                => $data['hash'],
1710                    'zbsi_pdf_template'        => $data['pdf_template'],
1711                    'zbsi_portal_template'     => $data['portal_template'],
1712                    'zbsi_email_template'      => $data['email_template'],
1713                    'zbsi_invoice_frequency'   => $data['invoice_frequency'],
1714                    'zbsi_currency'            => $data['currency'],
1715                    'zbsi_pay_via'             => $data['pay_via'],
1716                    'zbsi_logo_url'            => $data['logo_url'],
1717                    'zbsi_address_to_objtype'  => $data['address_to_objtype'],
1718                    'zbsi_addressed_from'      => $data['addressed_from'],
1719                    'zbsi_addressed_to'        => $data['addressed_to'],
1720                    'zbsi_allow_partial'       => $data['allow_partial'],
1721                    'zbsi_allow_tip'           => $data['allow_tip'],
1722                    'zbsi_send_attachments'    => $data['send_attachments'],
1723                    'zbsi_hours_or_quantity'   => $data['hours_or_quantity'],
1724                    'zbsi_date'                => $data['date'],
1725                    'zbsi_due_date'            => $data['due_date'],
1726                    'zbsi_paid_date'           => $data['paid_date'],
1727                    'zbsi_hash_viewed'         => $data['hash_viewed'],
1728                    'zbsi_hash_viewed_count'   => $data['hash_viewed_count'],
1729                    'zbsi_portal_viewed'       => $data['portal_viewed'],
1730                    'zbsi_portal_viewed_count' => $data['portal_viewed_count'],
1731                    'zbsi_net'                 => $data['net'],
1732                    'zbsi_discount'            => $data['discount'],
1733                    'zbsi_discount_type'       => $data['discount_type'],
1734                    'zbsi_shipping'            => $data['shipping'],
1735                    'zbsi_shipping_taxes'      => $data['shipping_taxes'],
1736                    'zbsi_shipping_tax'        => $data['shipping_tax'],
1737                    'zbsi_taxes'               => $data['taxes'],
1738                    'zbsi_tax'                 => $data['tax'],
1739                    'zbsi_total'               => $data['total'],
1740                    'zbsi_lastupdated'         => time(),
1741
1742                );
1743
1744                $typeArr = array( // field data types
1745                            // '%d',  // site
1746                            // '%d',  // team
1747                            // '%d',  // owner
1748
1749                    '%s', // id_override
1750                    '%d', // parent
1751                    '%s', // status
1752                    '%s', // hash
1753                    '%s', // pdf template
1754                    '%s', // portal template
1755                    '%s', // email template
1756                    '%d', // zbsi_invoice_frequency
1757                    '%s', // curr
1758                    '%d', // pay via
1759                    '%s', // logo url
1760                    '%d', // addr to obj type
1761                    '%s', // addr from
1762                    '%s', // addr to
1763                    '%d', // zbsi_allow_partial
1764                    '%d', // allow_tip
1765                    '%d', // hours or quantity
1766                    '%d', // zbsi_send_attachments
1767                    '%d', // date
1768                    '%d', // due date
1769                    '%d', // paid date
1770                    '%d', // hash viewed
1771                    '%d', // hash viewed count
1772                    '%d', // portal viewed
1773                    '%d', // portal viewed count
1774                    '%s',
1775                    '%s',
1776                    '%s',
1777                    '%s',
1778                    '%s',
1779                    '%s',
1780                    '%s',
1781                    '%s',
1782                    '%s',
1783                    '%d',
1784
1785                );
1786
1787                if ( ! empty( $id ) && $id > 0 ) {
1788
1789                    // is update
1790                    $update = true;
1791
1792                } else {
1793
1794                    // INSERT (get's few extra :D)
1795                    $update               = false;
1796                    $dataArr['zbs_site']  = zeroBSCRM_site();
1797                    $typeArr[]            = '%d';
1798                    $dataArr['zbs_team']  = zeroBSCRM_team();
1799                    $typeArr[]            = '%d';
1800                    $dataArr['zbs_owner'] = $owner;
1801                    $typeArr[]            = '%d';
1802                    if ( isset( $data['created'] ) && ! empty( $data['created'] ) && $data['created'] !== -1 ) {
1803                        $dataArr['zbsi_created'] = $data['created'];
1804                        $typeArr[]               = '%d';
1805                    } else {
1806                        $dataArr['zbsi_created'] = time();
1807                        $typeArr[]               = '%d';
1808                    }
1809                }
1810
1811                // if a blank hash is passed, generate a new one
1812                if ( isset( $dataArr['zbsi_hash'] ) && $dataArr['zbsi_hash'] == '' ) {
1813                    $dataArr['zbsi_hash'] = zeroBSCRM_generateHash( 20 );
1814                }
1815            }
1816
1817            #} ========= / BUILD DATA ===========
1818
1819            #} ============================================================
1820            #} ========= CHECK force_uniques & not_empty & max_len ========
1821
1822            // if we're passing limitedFields we skip these, for now
1823            // #v3.1 - would make sense to unique/nonempty check just the limited fields. #gh-145
1824            if ( ! is_array( $limitedFields ) ) {
1825
1826                // verify uniques
1827                if ( ! $this->verifyUniqueValues( $data, $id ) ) {
1828                    return false; // / fails unique field verify
1829                }
1830
1831                // verify not_empty
1832                if ( ! $this->verifyNonEmptyValues( $data ) ) {
1833                    return false; // / fails empty field verify
1834                }
1835            }
1836
1837            // whatever we do we check for max_len breaches and abbreviate to avoid wpdb rejections
1838            $dataArr = $this->wpdbChecks( $dataArr );
1839
1840            #} ========= / CHECK force_uniques & not_empty ================
1841            #} ============================================================
1842
1843            #} Check if ID present
1844            if ( $update ) {
1845
1846                #} Check if obj exists (here) - for now just brutal update (will error when doesn't exist)
1847                $originalStatus = $this->getInvoiceStatus( $id );
1848
1849                    $previous_invoice_obj = $this->getInvoice( $id );
1850
1851                // log any change of status
1852                if ( isset( $dataArr['zbsi_status'] ) && ! empty( $dataArr['zbsi_status'] ) && ! empty( $originalStatus ) && $dataArr['zbsi_status'] != $originalStatus ) {
1853
1854                    // status change
1855                    $statusChange = array(
1856                        'from' => $originalStatus,
1857                        'to'   => $dataArr['zbsi_status'],
1858                    );
1859                }
1860
1861                    // If we are using our CRM reference id (table field id_override) system, we should not change the reference number when importing from woo.
1862                if ( isset( $data['woo_use_crm_id'] ) && $data['woo_use_crm_id'] === true ) {
1863                    $dataArr['zbsi_id_override'] = $previous_invoice_obj['id_override']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1864                }
1865
1866                #} Attempt update
1867                if ( $wpdb->update(
1868                    $ZBSCRM_t['invoices'],
1869                    $dataArr,
1870                    array( // where
1871                        'ID' => $id,
1872                    ),
1873                    $typeArr,
1874                    array( // where data types
1875                        '%d',
1876                    )
1877                ) !== false ) {
1878
1879                        // if passing limitedFields instead of data, we ignore the following
1880                            // this doesn't work, because data is in args default as arr
1881                            // if (isset($data) && is_array($data)){
1882                            // so...
1883                    if ( ! isset( $limitedFields ) || ! is_array( $limitedFields ) || $limitedFields == -1 ) {
1884
1885                        // Line Items ====
1886
1887                        // line item work
1888                        if ( isset( $data['lineitems'] ) && is_array( $data['lineitems'] ) ) {
1889
1890                                // if array passed, update, even if removing
1891                            if ( count( $data['lineitems'] ) > 0 ) {
1892
1893                                // passed, for now this is BRUTAL and just clears old ones + readds
1894                                // once live, discuss how to refactor to be less brutal.
1895
1896                                // delete all lineitems
1897                                $this->DAL()->lineitems->deleteLineItemsForObject(
1898                                    array(
1899                                        'objID'   => $id,
1900                                        'objType' => ZBS_TYPE_INVOICE,
1901                                    )
1902                                );
1903
1904                                // addupdate each
1905                                foreach ( $data['lineitems'] as $lineitem ) {
1906
1907                                    // slight rejig of passed so works cleanly with data array style
1908                                    $lineItemID = false;
1909                                    if ( isset( $lineitem['ID'] ) ) {
1910                                        $lineItemID = $lineitem['ID'];
1911                                    }
1912                                    $this->DAL()->lineitems->addUpdateLineitem(
1913                                        array(
1914                                            'id'          => $lineItemID,
1915                                            'linkedObjType' => ZBS_TYPE_INVOICE,
1916                                            'linkedObjID' => $id,
1917                                            'data'        => $lineitem,
1918                                            'calculate_totals' => true,
1919                                        )
1920                                    );
1921
1922                                }
1923                            } else {
1924
1925                                // delete all lineitems
1926                                $this->DAL()->lineitems->deleteLineItemsForObject(
1927                                    array(
1928                                        'objID'   => $id,
1929                                        'objType' => ZBS_TYPE_INVOICE,
1930                                    )
1931                                );
1932
1933                            }
1934                        }
1935
1936                        // / Line Items ====
1937
1938                        // OBJ LINKS - to contacts/companies
1939                        if ( ! is_array( $data['contacts'] ) ) {
1940                            $this->addUpdateObjectLinks( $id, 'unset', ZBS_TYPE_CONTACT );
1941                        } else {
1942                            $this->addUpdateObjectLinks( $id, $data['contacts'], ZBS_TYPE_CONTACT );
1943                        }
1944                        if ( ! is_array( $data['companies'] ) ) {
1945                            $this->addUpdateObjectLinks( $id, 'unset', ZBS_TYPE_COMPANY );
1946                        } else {
1947                            $this->addUpdateObjectLinks( $id, $data['companies'], ZBS_TYPE_COMPANY );
1948                        }
1949
1950                        // IA also gets 'againstid' historically, but we'll pass as 'against id's'
1951                        $againstIDs = array(
1952                            'contacts'  => $data['contacts'],
1953                            'companies' => $data['companies'],
1954                        );
1955
1956                        // tags
1957                        if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
1958
1959                                    $this->addUpdateInvoiceTags(
1960                                        array(
1961                                            'id'        => $id,
1962                                            'tag_input' => $data['tags'],
1963                                            'mode'      => $data['tag_mode'],
1964                                        )
1965                                    );
1966
1967                        }
1968
1969                                // externalSources
1970                                $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
1971                                    array(
1972                                        'obj_id'           => $id,
1973                                        'obj_type_id'      => ZBS_TYPE_INVOICE,
1974                                        'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
1975                                    )
1976                                ); // for IA below
1977
1978                        // Custom fields?
1979
1980                        #} Cycle through + add/update if set
1981                        if ( is_array( $customFields ) ) {
1982                            foreach ( $customFields as $cK => $cF ) {
1983
1984                                // any?
1985                                if ( isset( $data[ $cK ] ) ) {
1986
1987                                    // add update
1988                                    $cfID = $this->DAL()->addUpdateCustomField(
1989                                        array(
1990                                            'data' => array(
1991                                                'objtype' => ZBS_TYPE_INVOICE,
1992                                                'objid'   => $id,
1993                                                'objkey'  => $cK,
1994                                                'objval'  => $data[ $cK ],
1995                                            ),
1996                                        )
1997                                    );
1998
1999                                }
2000                            }
2001                        }
2002
2003                        // / Custom Fields
2004
2005                    } // / if $data
2006
2007                        #} Any extra meta keyval pairs?
2008                        // BRUTALLY updates (no checking)
2009                        $confirmedExtraMeta = false;
2010                    if ( is_array( $extraMeta ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2011
2012                            $confirmedExtraMeta = array();
2013
2014                        foreach ( $extraMeta as $k => $v ) {
2015
2016                            #} This won't fix stupid keys, just catch basic fails...
2017                            $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
2018
2019                            #} Brutal update
2020                            // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
2021                            $this->DAL()->updateMeta( ZBS_TYPE_INVOICE, $id, 'extra_' . $cleanKey, $v );
2022
2023                            #} Add it to this, which passes to IA
2024                            $confirmedExtraMeta[ $cleanKey ] = $v;
2025
2026                        }
2027                    }
2028
2029                        #} INTERNAL AUTOMATOR
2030                        #} &
2031                        #} FALLBACKS
2032                        // UPDATING CONTACT
2033                    if ( ! $silentInsert ) {
2034
2035                        // catch dirty flag (update of status) (note, after update_post_meta - as separate)
2036                        // if (isset($_POST['zbsi_status_dirtyflag']) && $_POST['zbsi_status_dirtyflag'] == "1"){
2037                        // actually here, it's set above
2038                        if ( isset( $statusChange ) && is_array( $statusChange ) ) {
2039
2040                            // status has changed
2041
2042                            // IA
2043                            zeroBSCRM_FireInternalAutomator(
2044                                'invoice.status.update',
2045                                array(
2046                                    'id'         => $id,
2047                                    'againstids' => array(), // $againstIDs,
2048                                    'data'       => $data,
2049                                    'from'       => $statusChange['from'],
2050                                    'to'         => $statusChange['to'],
2051                                )
2052                            );
2053
2054                        }
2055
2056                        // IA General invoice update (2.87+)
2057                        zeroBSCRM_FireInternalAutomator(
2058                            'invoice.update',
2059                            array(
2060                                'id'                   => $id,
2061                                'data'                 => $data,
2062                                'againstids'           => array(), // $againstIDs,
2063                                'extsource'            => false, // $approvedExternalSource
2064                                'automatorpassthrough' => $automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
2065                                'extraMeta'            => $confirmedExtraMeta, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase -- Also this is the "extraMeta" passed (as saved)
2066                            'prev_invoice'             => $previous_invoice_obj,
2067                            )
2068                        );
2069
2070                                    $data['id']                 = $id;
2071                                    $previous_invoice_obj['id'] = $id;
2072
2073                                    $this->events_manager->invoice()->updated( $data, $previous_invoice_obj );
2074
2075                    }
2076
2077                        // Successfully updated - Return id
2078                        return $id;
2079
2080                } else {
2081
2082                    $msg = __( 'DB Update Failed', 'zero-bs-crm' );
2083                    $zbs->DAL->addError( 302, $this->objectType, $msg, $dataArr );
2084
2085                    // FAILED update
2086                    return false;
2087
2088                }
2089            } else {
2090                // If we are using our CRM reference id (table field id_override) system, we should generate a new invoice number.
2091                if ( isset( $data['woo_use_crm_id'] ) && $data['woo_use_crm_id'] === true ) {
2092                    $ref_type = $zbs->settings->get( 'reftype' );
2093                    // We can only generate it if autonumber is set
2094                    if ( $ref_type === 'autonumber' ) {
2095                        $next_number                 = $zbs->settings->get( 'refnextnum' );
2096                        $dataArr['zbsi_id_override'] = $zbs->settings->get( 'refprefix' ) . $next_number . $zbs->settings->get( 'refsuffix' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2097                        ++$next_number;
2098                        $zbs->settings->update( 'refnextnum', $next_number );
2099                    }
2100                }
2101
2102                #} No ID - must be an INSERT
2103                if ( $wpdb->insert(
2104                    $ZBSCRM_t['invoices'],
2105                    $dataArr,
2106                    $typeArr
2107                ) > 0 ) {
2108
2109                    #} Successfully inserted, lets return new ID
2110                    $newID = $wpdb->insert_id;
2111
2112                    // Line Items ====
2113
2114                    // line item work
2115                    if ( isset( $data['lineitems'] ) && is_array( $data['lineitems'] ) ) {
2116
2117                        // if array passed, update, even if removing
2118                        if ( count( $data['lineitems'] ) > 0 ) {
2119
2120                            // passed, for now this is BRUTAL and just clears old ones + readds
2121                            // once live, discuss how to refactor to be less brutal.
2122
2123                                // delete all lineitems
2124                                $this->DAL()->lineitems->deleteLineItemsForObject(
2125                                    array(
2126                                        'objID'   => $newID,
2127                                        'objType' => ZBS_TYPE_INVOICE,
2128                                    )
2129                                );
2130
2131                                // addupdate each
2132                            foreach ( $data['lineitems'] as $lineitem ) {
2133
2134                                // slight rejig of passed so works cleanly with data array style
2135                                $lineItemID = false;
2136                                if ( isset( $lineitem['ID'] ) ) {
2137                                    $lineItemID = $lineitem['ID'];
2138                                }
2139                                $this->DAL()->lineitems->addUpdateLineitem(
2140                                    array(
2141                                        'id'               => $lineItemID,
2142                                        'linkedObjType'    => ZBS_TYPE_INVOICE,
2143                                        'linkedObjID'      => $newID,
2144                                        'data'             => $lineitem,
2145                                        'calculate_totals' => true,
2146                                    )
2147                                );
2148
2149                            }
2150                        } else {
2151
2152                            // delete all lineitems
2153                            $this->DAL()->lineitems->deleteLineItemsForObject(
2154                                array(
2155                                    'objID'   => $newID,
2156                                    'objType' => ZBS_TYPE_INVOICE,
2157                                )
2158                            );
2159
2160                        }
2161                    }
2162
2163                    // / Line Items ====
2164
2165                    // OBJ LINKS - to contacts/companies
2166                    $this->addUpdateObjectLinks( $newID, $data['contacts'], ZBS_TYPE_CONTACT );
2167                    $this->addUpdateObjectLinks( $newID, $data['companies'], ZBS_TYPE_COMPANY );
2168                    // IA also gets 'againstid' historically, but we'll pass as 'against id's'
2169                    $againstIDs = array(
2170                        'contacts'  => $data['contacts'],
2171                        'companies' => $data['companies'],
2172                    );
2173
2174                    // tags
2175                    if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) {
2176
2177                        $this->addUpdateInvoiceTags(
2178                            array(
2179                                'id'        => $newID,
2180                                'tag_input' => $data['tags'],
2181                                'mode'      => $data['tag_mode'],
2182                            )
2183                        );
2184
2185                    }
2186
2187                    // externalSources
2188                    $approvedExternalSource = $this->DAL()->addUpdateExternalSources(
2189                        array(
2190                            'obj_id'           => $newID,
2191                            'obj_type_id'      => ZBS_TYPE_INVOICE,
2192                            'external_sources' => isset( $data['externalSources'] ) ? $data['externalSources'] : array(),
2193                        )
2194                    ); // for IA below
2195
2196                        // Custom fields?
2197
2198                        #} Cycle through + add/update if set
2199                    if ( is_array( $customFields ) ) {
2200                        foreach ( $customFields as $cK => $cF ) {
2201
2202                            // any?
2203                            if ( isset( $data[ $cK ] ) ) {
2204                                // add update
2205                                $cfID = $this->DAL()->addUpdateCustomField(
2206                                    array(
2207                                        'data' => array(
2208                                            'objtype' => ZBS_TYPE_INVOICE,
2209                                            'objid'   => $newID,
2210                                            'objkey'  => $cK,
2211                                            'objval'  => $data[ $cK ],
2212                                        ),
2213                                    )
2214                                );
2215                            }
2216                        }
2217                    }
2218
2219                        // / Custom Fields
2220
2221                        #} Any extra meta keyval pairs?
2222                        // BRUTALLY updates (no checking)
2223                        $confirmedExtraMeta = false;
2224                    if ( is_array( $extraMeta ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
2225
2226                        $confirmedExtraMeta = array();
2227
2228                        foreach ( $extraMeta as $k => $v ) {
2229
2230                            #} This won't fix stupid keys, just catch basic fails...
2231                            $cleanKey = strtolower( str_replace( ' ', '_', $k ) );
2232
2233                            #} Brutal update
2234                            // update_post_meta($postID, 'zbs_customer_extra_'.$cleanKey, $v);
2235                            $this->DAL()->updateMeta( ZBS_TYPE_INVOICE, $newID, 'extra_' . $cleanKey, $v );
2236
2237                            #} Add it to this, which passes to IA
2238                            $confirmedExtraMeta[ $cleanKey ] = $v;
2239
2240                        }
2241                    }
2242
2243                        #} INTERNAL AUTOMATOR
2244                        #} &
2245                        #} FALLBACKS
2246                        // NEW CONTACT
2247                    if ( ! $silentInsert ) {
2248
2249                        #} Add to automator
2250                        zeroBSCRM_FireInternalAutomator(
2251                            'invoice.new',
2252                            array(
2253                                'id'                   => $newID,
2254                                'data'                 => $data,
2255                                'againstids'           => $againstIDs,
2256                                'extsource'            => $approvedExternalSource,
2257                                'automatorpassthrough' => $automatorPassthrough, #} This passes through any custom log titles or whatever into the Internal automator recipe.
2258                                'extraMeta'            => $confirmedExtraMeta, #} This is the "extraMeta" passed (as saved)
2259                            )
2260                        );
2261
2262                                            $data['id'] = $newID; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
2263                                            $this->events_manager->invoice()->created( $data );
2264                    }
2265
2266                        return $newID;
2267
2268                } else {
2269
2270                    $msg = __( 'DB Insert Failed', 'zero-bs-crm' );
2271                    $zbs->DAL->addError( 303, $this->objectType, $msg, $dataArr );
2272
2273                    #} Failed to Insert
2274                    return false;
2275
2276                }
2277            }
2278
2279            return false;
2280        }
2281
2282        /**
2283         * adds or updates a invoice's tags
2284         * ... this is really just a wrapper for addUpdateObjectTags
2285         *
2286         * @param array $args Associative array of arguments
2287         *              id (if update), owner, data (array of field data)
2288         *
2289         * @return int line ID
2290         */
2291        public function addUpdateInvoiceTags( $args = array() ) {
2292
2293            global $ZBSCRM_t, $wpdb;
2294
2295            #} ============ LOAD ARGS =============
2296            $defaultArgs = array(
2297
2298                'id'        => -1,
2299
2300                // generic pass-through (array of tag strings or tag IDs):
2301                'tag_input' => -1,
2302
2303                // or either specific:
2304                'tagIDs'    => -1,
2305                'tags'      => -1,
2306
2307                'mode'      => 'append',
2308
2309            );
2310            foreach ( $defaultArgs as $argK => $argV ) {
2311                $$argK = $argV;
2312                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2313                    if ( is_array( $args[ $argK ] ) ) {
2314                        $newData = $$argK;
2315                        if ( ! is_array( $newData ) ) {
2316                            $newData = array();
2317                        } foreach ( $args[ $argK ] as $subK => $subV ) {
2318                            $newData[ $subK ] = $subV;
2319                        }$$argK = $newData;
2320                    } else {
2321                        $$argK = $args[ $argK ]; }
2322                }
2323            }
2324            #} =========== / LOAD ARGS ============
2325
2326            #} ========== CHECK FIELDS ============
2327
2328            // check id
2329            $id = (int) $id;
2330            if ( empty( $id ) || $id <= 0 ) {
2331                return false;
2332            }
2333
2334            #} ========= / CHECK FIELDS ===========
2335
2336            return $this->DAL()->addUpdateObjectTags(
2337                array(
2338                    'objtype'   => ZBS_TYPE_INVOICE,
2339                    'objid'     => $id,
2340                    'tag_input' => $tag_input,
2341                    'tags'      => $tags,
2342                    'tagIDs'    => $tagIDs,
2343                    'mode'      => $mode,
2344                )
2345            );
2346        }
2347
2348        /**
2349         * updates status for an invoice (no blanks allowed)
2350         *
2351         * @param int id Invoice ID
2352         * @param string Invoice Status
2353         *
2354         * @return bool
2355         */
2356        public function setInvoiceStatus( $id = -1, $status = -1 ) {
2357
2358            global $zbs;
2359
2360            $id = (int) $id;
2361
2362            if ( $id > 0 && ! empty( $status ) && $status !== -1 ) {
2363
2364                return $this->addUpdateInvoice(
2365                    array(
2366                        'id'            => $id,
2367                        'limitedFields' => array(
2368                            array(
2369                                'key'  => 'zbsi_status',
2370                                'val'  => $status,
2371                                'type' => '%s',
2372                            ),
2373                        ),
2374                    )
2375                );
2376
2377            }
2378
2379            return false;
2380        }
2381
2382        /**
2383         * deletes a invoice object
2384         *
2385         * @param array $args Associative array of arguments
2386         *              id
2387         *
2388         * @return int success;
2389         */
2390        public function deleteInvoice( $args = array() ) {
2391
2392            global $ZBSCRM_t, $wpdb, $zbs;
2393
2394            #} ============ LOAD ARGS =============
2395            $defaultArgs = array(
2396
2397                'id'          => -1,
2398                'saveOrphans' => false,
2399
2400            );
2401            foreach ( $defaultArgs as $argK => $argV ) {
2402                $$argK = $argV;
2403                if ( is_array( $args ) && isset( $args[ $argK ] ) ) {
2404                    if ( is_array( $args[ $argK ] ) ) {
2405                        $newData = $$argK;
2406                        if ( ! is_array( $newData ) ) {
2407                            $newData = array();
2408                        } foreach ( $args[ $argK ] as $subK => $subV ) {
2409                            $newData[ $subK ] = $subV;
2410                        }$$argK = $newData;
2411                    } else {
2412                        $$argK = $args[ $argK ]; }
2413                }
2414            }
2415            #} =========== / LOAD ARGS ============
2416
2417            #} Check ID & Delete :)
2418            $id = (int) $id;
2419            if ( ! empty( $id ) && $id > 0 ) {
2420
2421                // delete orphans?
2422                if ( $saveOrphans === false ) {
2423
2424                    // delete any tag links
2425                    $this->DAL()->deleteTagObjLinks(
2426                        array(
2427
2428                            'objtype' => ZBS_TYPE_INVOICE,
2429                            'objid'   => $id,
2430                        )
2431                    );
2432
2433                    // delete any external source information
2434                    $this->DAL()->delete_external_sources(
2435                        array(
2436
2437                            'obj_type'   => ZBS_TYPE_INVOICE,
2438                            'obj_id'     => $id,
2439                            'obj_source' => 'all',
2440
2441                        )
2442                    );
2443
2444                    // delete any links to contacts
2445                    $this->DAL()->deleteObjLinks(
2446                        array(
2447
2448                            'objtypefrom' => ZBS_TYPE_INVOICE,
2449                            'objtypeto'   => ZBS_TYPE_CONTACT,
2450                            'objtofrom'   => $id,
2451
2452                        )
2453                    );
2454
2455                    // delete any links to transactions
2456                    $this->DAL()->deleteObjLinks(
2457                        array(
2458
2459                            'objtypefrom' => ZBS_TYPE_TRANSACTION,
2460                            'objtypeto'   => ZBS_TYPE_INVOICE,
2461                            'objtoid'     => $id,
2462
2463                        )
2464                    );
2465
2466                    // delete all orphaned lineitems
2467                    $this->DAL()->lineitems->deleteLineItemsForObject(
2468                        array(
2469                            'objID'   => $id,
2470                            'objType' => ZBS_TYPE_INVOICE,
2471                        )
2472                    );
2473
2474                    // delete all orphaned line items obj links
2475                    $this->DAL()->deleteObjLinks(
2476                        array(
2477
2478                            'objtypefrom' => ZBS_TYPE_LINEITEM,
2479                            'objtypeto'   => ZBS_TYPE_INVOICE,
2480                            'objtoid'     => $id,
2481
2482                        )
2483                    );
2484
2485                }
2486
2487                $del = zeroBSCRM_db2_deleteGeneric( $id, 'invoices' );
2488
2489                #} Add to automator
2490                zeroBSCRM_FireInternalAutomator(
2491                    'invoice.delete',
2492                    array(
2493                        'id'          => $id,
2494                        'saveOrphans' => $saveOrphans,
2495                    )
2496                );
2497
2498                return $del;
2499
2500            }
2501
2502            return false;
2503        }
2504
2505        /**
2506         * tidy's the object from wp db into clean array
2507         *
2508         * @param array $obj (DB obj)
2509         *
2510         * @return array invoice (clean obj)
2511         */
2512        private function tidy_invoice( $obj = false, $withCustomFields = false ) {
2513
2514            $res = false;
2515
2516            if ( isset( $obj->ID ) ) {
2517                $res       = array();
2518                $res['id'] = $obj->ID;
2519                /*
2520                `zbs_site` INT NULL DEFAULT NULL,
2521                `zbs_team` INT NULL DEFAULT NULL,
2522                `zbs_owner` INT NOT NULL,
2523                */
2524                $res['owner'] = $obj->zbs_owner;
2525
2526                $res['id_override']         = $this->stripSlashes( $obj->zbsi_id_override );
2527                $res['parent']              = (int) $obj->zbsi_parent;
2528                $res['status']              = $this->stripSlashes( $obj->zbsi_status );
2529                    $res['status_label']    = __( $res['status'], 'zero-bs-crm' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
2530                $res['hash']                = $this->stripSlashes( $obj->zbsi_hash );
2531                $res['pdf_template']        = $this->stripSlashes( $obj->zbsi_pdf_template );
2532                $res['portal_template']     = $this->stripSlashes( $obj->zbsi_portal_template );
2533                $res['email_template']      = $this->stripSlashes( $obj->zbsi_email_template );
2534                $res['invoice_frequency']   = (int) $obj->zbsi_invoice_frequency;
2535                $res['currency']            = $this->stripSlashes( $obj->zbsi_currency );
2536                $res['pay_via']             = (int) $obj->zbsi_pay_via;
2537                $res['logo_url']            = $this->stripSlashes( $obj->zbsi_logo_url );
2538                $res['address_to_objtype']  = (int) $obj->zbsi_address_to_objtype;
2539                $res['addressed_from']      = $this->stripSlashes( $obj->zbsi_addressed_from );
2540                $res['addressed_to']        = $this->stripSlashes( $obj->zbsi_addressed_to );
2541                $res['allow_partial']       = (bool) $obj->zbsi_allow_partial;
2542                $res['allow_tip']           = (bool) $obj->zbsi_allow_tip;
2543                $res['send_attachments']    = (bool) $obj->zbsi_send_attachments;
2544                $res['hours_or_quantity']   = $this->stripSlashes( $obj->zbsi_hours_or_quantity );
2545                $res['date']                = (int) $obj->zbsi_date;
2546                $res['date_date']           = ( isset( $obj->zbsi_date ) && $obj->zbsi_date > 0 ) ? jpcrm_uts_to_date_str( $obj->zbsi_date ) : false;
2547                $res['due_date']            = (int) $obj->zbsi_due_date;
2548                $res['due_date_date']       = ( isset( $obj->zbsi_due_date ) && $obj->zbsi_due_date > 0 ) ? jpcrm_uts_to_date_str( $obj->zbsi_due_date ) : false;
2549                $res['paid_date']           = (int) $obj->zbsi_paid_date;
2550                $res['paid_date_date']      = ( isset( $obj->zbsi_paid_date ) && $obj->zbsi_paid_date > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsi_paid_date, false, true ) : false;
2551                $res['hash_viewed']         = (int) $obj->zbsi_hash_viewed;
2552                $res['hash_viewed_date']    = ( isset( $obj->zbsi_hash_viewed ) && $obj->zbsi_hash_viewed > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsi_hash_viewed, false, true ) : false;
2553                $res['hash_viewed_count']   = (int) $obj->zbsi_hash_viewed_count;
2554                $res['portal_viewed']       = (int) $obj->zbsi_portal_viewed;
2555                $res['portal_viewed_date']  = ( isset( $obj->zbsi_portal_viewed ) && $obj->zbsi_portal_viewed > 0 ) ? zeroBSCRM_date_i18n( -1, $obj->zbsi_portal_viewed, false, true ) : false;
2556                $res['portal_viewed_count'] = (int) $obj->zbsi_portal_viewed_count;
2557                $res['net']                 = $this->stripSlashes( $obj->zbsi_net );
2558                $res['discount']            = $this->stripSlashes( $obj->zbsi_discount );
2559                $res['discount_type']       = $this->stripSlashes( $obj->zbsi_discount_type );
2560                $res['shipping']            = $this->stripSlashes( $obj->zbsi_shipping );
2561                $res['shipping_taxes']      = $this->stripSlashes( $obj->zbsi_shipping_taxes );
2562                $res['shipping_tax']        = $this->stripSlashes( $obj->zbsi_shipping_tax );
2563                $res['taxes']               = $this->stripSlashes( $obj->zbsi_taxes );
2564                $res['tax']                 = $this->stripSlashes( $obj->zbsi_tax );
2565                $res['total']               = $this->stripSlashes( $obj->zbsi_total );
2566                $res['created']             = (int) $obj->zbsi_created;
2567                $res['created_date']        = ( isset( $obj->zbsi_created ) && $obj->zbsi_created > 0 ) ? zeroBSCRM_locale_utsToDatetime( $obj->zbsi_created ) : false;
2568                $res['lastupdated']         = (int) $obj->zbsi_lastupdated;
2569                $res['lastupdated_date']    = ( isset( $obj->zbsi_lastupdated ) && $obj->zbsi_lastupdated > 0 ) ? zeroBSCRM_locale_utsToDatetime( $obj->zbsi_lastupdated ) : false;
2570
2571                // custom fields - tidy any that are present:
2572                if ( $withCustomFields ) {
2573                    $res = $this->tidyAddCustomFields( ZBS_TYPE_INVOICE, $obj, $res, false );
2574                }
2575            }
2576
2577            return $res;
2578        }
2579
2580        /**
2581         * Takes whatever invoice data available and re-calculates net, total, tax etc.
2582         * .. returning same obj with updated vals
2583         * .. This is a counter to the js func which does this in-UI, so changes need to be replicated in either or
2584         *
2585         * @param array $invoice_data Invoice data.
2586         *
2587         * @return array
2588         */
2589        public function recalculate( $invoice_data = false ) {
2590
2591            if ( is_array( $invoice_data ) ) {
2592                global $zbs;
2593
2594                // we pass any discount saved against main invoice DOWN to the lineitems, first:
2595                if ( isset( $invoice_data['lineitems'] ) && is_array( $invoice_data['lineitems'] ) ) {
2596                    $final_line_items = array();
2597
2598                    // if not discounted, still recalc net
2599                    if ( ! isset( $invoice_data['discount'] ) && $invoice_data['discount'] <= 0 ) {
2600                        // recalc line items, but no discount to apply
2601                        foreach ( $invoice_data['lineitems'] as $line_item ) {
2602                            // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2603                            $final_line_items[] = $zbs->DAL->lineitems->recalculate( $line_item );
2604                        }
2605                    } else {
2606                        $discount_value = (float) $invoice_data['discount'];
2607                        $discount_type  = 'value';
2608
2609                        if ( $invoice_data['discount_type'] === '%' ) {
2610                            $discount_type = 'percentage';
2611                        }
2612                        $calc_line_items = array();
2613
2614                        if ( $discount_type === 'percentage' ) {
2615                            $discount_percentage = ( (float) $invoice_data['discount'] ) / 100;
2616
2617                            // percentage discount
2618                            foreach ( $invoice_data['lineitems'] as $line_item ) {
2619                                $n = $line_item;
2620                                if ( ! isset( $line_item['fee'] ) ) {
2621                                    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2622                                    $n = $zbs->DAL->lineitems->recalculate( $line_item );
2623
2624                                    $n['discount'] = $n['net'] * $discount_percentage;
2625                                    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2626                                    $n = $zbs->DAL->lineitems->recalculate( $n );
2627                                } else {
2628                                    $n['net'] = $line_item['total'];
2629                                }
2630                                $final_line_items[] = $n;
2631                            }
2632                        } else {
2633                            // first calc +
2634                            // accumulate a line-item net, so can pro-rata discounts
2635                            $line_item_sum = 0;
2636
2637                            foreach ( $invoice_data['lineitems'] as $line_item ) {
2638                                if ( ! isset( $line_item['fee'] ) ) {
2639                                    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2640                                    $n = $zbs->DAL->lineitems->recalculate( $line_item );
2641
2642                                    $line_item_sum    += $n['net'];
2643                                    $calc_line_items[] = $n;
2644                                } else {
2645                                    $line_item['net']  = $line_item['total'];
2646                                    $calc_line_items[] = $line_item;
2647                                }
2648                            }
2649
2650                            // now actually correct em
2651                            foreach ( $calc_line_items as $n ) {
2652                                $nl = $n;
2653
2654                                if ( ! isset( $n['fee'] ) ) {
2655                                    // calc pro-rata discount in absolute 0.00
2656                                    // so this takes the net of all line item values
2657                                    // and then proportionally discounts a part of it (this line item net)
2658                                    // ... where have net
2659                                    if ( $n['net'] > 0 && $line_item_sum > 0 ) {
2660                                        $nl['discount'] = round( $discount_value * ( $n['net'] / $line_item_sum ), 2 );
2661                                    }
2662
2663                                    // final recalc to deal with new discount val
2664                                    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2665                                    $nl = $zbs->DAL->lineitems->recalculate( $nl );
2666                                }
2667                                // pass it
2668                                $final_line_items[] = $nl;
2669                            }
2670                        } // / absolute discount
2671                    } // / if lineitems + discount to apply
2672
2673                    // reset
2674                    $invoice_data['lineitems'] = $final_line_items;
2675                    unset( $final_line_items, $calc_line_items );
2676                }
2677
2678                // subtotal (zbsi_net)
2679                // == line item Quantity * rate * tax%
2680                // ... also calc tax as we go for 'total' below
2681                $sub_total = 0.0;
2682                $items_tax = 0.0;
2683                $discount  = 0.0;
2684
2685                // cycle through (any) line items
2686                if ( isset( $invoice_data ) && is_array( $invoice_data['lineitems'] ) ) {
2687                    foreach ( $invoice_data['lineitems'] as $line_item ) {
2688                        // can then directly use the recalced numbers :)
2689                        if ( isset( $line_item['net'] ) ) {
2690                            $sub_total += $line_item['net'];
2691                        }
2692                        if ( isset( $line_item['discount'] ) ) {
2693                            $discount += (float) $line_item['discount'];
2694                        }
2695                        $items_tax += $line_item['tax'];
2696                    }
2697                }
2698
2699                // set it
2700                $invoice_data['net'] = $sub_total;
2701
2702                // discount is accumulated from line items (applied at top)
2703                $total = $invoice_data['net'] - $discount;
2704
2705                $shipping        = 0.0;
2706                $tax_on_shipping = 0.0;
2707
2708                // shipping subtotal
2709                if ( isset( $invoice_data['shipping'] ) && ! empty( $invoice_data['shipping'] ) ) {
2710                    $shipping = (float) $invoice_data['shipping'];
2711                }
2712
2713                if ( $shipping > 0 && empty( $invoice_data['shipping_tax'] ) ) {
2714                    // tax on shipping - recalc.
2715                    if ( isset( $invoice_data['shipping_taxes'] ) ) {
2716                        $tax_on_shipping = zeroBSCRM_taxRates_getTaxValue( $shipping, $invoice_data['shipping_taxes'] );
2717                    }
2718
2719                    // set it
2720                    $invoice_data['shipping_tax'] = $tax_on_shipping;
2721
2722                    // shipping total
2723                    $shipping += $tax_on_shipping;
2724
2725                } elseif ( is_numeric( $invoice_data['shipping_tax'] ) ) {
2726                    $shipping += $invoice_data['shipping_tax'];
2727                }
2728                $total += $shipping;
2729
2730                // tax - this is (re)calculated by line item recalc above
2731                $total += $items_tax;
2732
2733                // total tax for invoice = lineitem tax + any tax on shipping
2734                $invoice_data['tax'] = $items_tax + $tax_on_shipping;
2735
2736                // set it
2737                $invoice_data['total'] = $total;
2738
2739                return $invoice_data;
2740            }
2741            return false;
2742        }
2743
2744        /**
2745         * Takes whatever invoice data available and generates correct totals table (discount, shipping, tax vals)
2746         *
2747         * @param array $invoiceData
2748         *
2749         * @return array $invoiceTotals
2750         */
2751        public function generateTotalsTable( $invoice = false ) {
2752
2753            $totals = array(
2754
2755                // not req. as part of main obj 'net'
2756                'discount' => 0.0,
2757                'taxes'    => array(),
2758            // not req. as part of main obj 'shipping'
2759            // not req. as part of main obj 'total'
2760            );
2761
2762            // settings
2763            global $zbs;
2764            $invsettings = $zbs->settings->getAll();
2765
2766            // Discount
2767            if ( isset( $invoice['discount'] ) && ! empty( $invoice['discount'] ) ) {
2768
2769                if ( $invsettings['invdis'] == 1 ) {
2770
2771                    // v3.0+ we have discount type ('m' or '%')
2772                    $discountType = 'value'; if ( isset( $invoice['discount_type'] ) && ! empty( $invoice['discount_type'] ) ) {
2773
2774                        if ( $invoice['discount_type'] == '%' ) {
2775                            $discountType = 'percentage';
2776                        }
2777                    }
2778
2779                    if ( $discountType == 'value' ) {
2780
2781                        // value out $£
2782                        if ( isset( $invoice['discount'] ) && ! empty( $invoice['discount'] ) ) {
2783
2784                            $totals['discount'] = $invoice['discount'];
2785
2786                        }
2787                    } else {
2788
2789                        // percentage out - calc
2790                        if ( isset( $invoice['discount'] ) && ! empty( $invoice['discount'] ) && isset( $invoice['net'] ) ) {
2791
2792                            $discountAmount = 0;
2793                            $invDiscount    = (float) $invoice['discount'];
2794                            if ( $invDiscount > 0 ) {
2795                                $discountAmount = ( $invDiscount / 100 ) * $invoice['net'];
2796                            }
2797
2798                            $totals['discount'] = $discountAmount;
2799
2800                        }
2801                    }
2802                }
2803            }
2804
2805            if ( $invsettings['invtax'] == 1 ) {
2806
2807                // this output's tax in 1 number
2808                // if(isset($invoice["tax"]) && !empty($invoice["tax"])){ $totalsTable .= zeroBSCRM_formatCurrency($invoice["tax"]); }else{ $totalsTable .= zeroBSCRM_formatCurrency(0); }
2809                // ... but local taxes need splitting, so recalc & display by lineitems.
2810                $taxLines = false; if ( isset( $invoice['lineitems'] ) && is_array( $invoice['lineitems'] ) && count( $invoice['lineitems'] ) > 0 ) {
2811
2812                    // here we use this summarising func to retrieve
2813                    $taxLines = $zbs->DAL->lineitems->getLineitemsTaxSummary( array( 'lineItems' => $invoice['lineitems'] ) );
2814
2815                }
2816
2817                // add any shipping tax :)
2818                if ( $invsettings['invpandp'] == 1 ) {
2819
2820                    // shipping (if used)
2821                    $shippingV     = 0.0;
2822                    $taxOnShipping = 0.0;
2823
2824                    // shipping subtotal
2825                    if ( isset( $invoice['shipping'] ) && ! empty( $invoice['shipping'] ) ) {
2826
2827                        $shippingV = (float) $invoice['shipping'];
2828
2829                    }
2830
2831                    if ( $shippingV > 0 ) {
2832
2833                        // tax on shipping - recalc.
2834                        if ( isset( $invoice['shipping_taxes'] ) ) {
2835                            $taxOnShipping = zeroBSCRM_taxRates_getTaxValue( $shippingV, $invoice['shipping_taxes'] );
2836                        }
2837
2838                        // shipping can only have 1 tax at the moment, so find that tax and add to summary:
2839                        $shippingRate = zeroBSCRM_taxRates_getTaxRate( $invoice['shipping_taxes'] );
2840
2841                        if ( is_array( $shippingRate ) && isset( $shippingRate['id'] ) ) {
2842
2843                            // add to summary
2844                            if ( ! isset( $taxLines[ $shippingRate['id'] ] ) ) {
2845
2846                                // new, add
2847                                $taxLines[ $shippingRate['id'] ] = array(
2848
2849                                    'name'  => $shippingRate['name'],
2850                                    'rate'  => $shippingRate['rate'],
2851                                    'value' => $taxOnShipping,
2852
2853                                );
2854
2855                            } else {
2856
2857                                // +=
2858                                $taxLines[ $shippingRate['id'] ]['value'] += $taxOnShipping;
2859
2860                            }
2861                        }
2862                    }
2863                }
2864
2865                if ( isset( $taxLines ) && is_array( $taxLines ) && count( $taxLines ) > 0 ) {
2866
2867                    $totals['taxes'] = $taxLines;
2868
2869                } else {
2870
2871                    // simple fallback
2872                    // ...just use $invoice["tax"]
2873
2874                }
2875            }
2876
2877            return $totals;
2878        }
2879
2880        /**
2881         * Wrapper, use $this->getInvoiceMeta($contactID,$key) for easy retrieval of singular invoice
2882         * Simplifies $this->getMeta
2883         *
2884         * @param int objtype
2885         * @param int objid
2886         * @param string key
2887         *
2888         * @return array invoice meta result
2889         */
2890        public function getInvoiceMeta( $id = -1, $key = '', $default = false ) {
2891
2892            global $zbs;
2893
2894            if ( ! empty( $key ) ) {
2895
2896                return $this->DAL()->getMeta(
2897                    array(
2898
2899                        'objtype'     => ZBS_TYPE_INVOICE,
2900                        'objid'       => $id,
2901                        'key'         => $key,
2902                        'fullDetails' => false,
2903                        'default'     => $default,
2904                        'ignoreowner' => true, // for now !!
2905
2906                    )
2907                );
2908
2909            }
2910
2911            return $default;
2912        }
2913
2914        /**
2915         * Returns an ownerid against a invoice
2916         *
2917         * @param int id invoice ID
2918         *
2919         * @return int invoice owner id
2920         */
2921        public function getInvoiceOwner( $id = -1 ) {
2922
2923            global $zbs;
2924
2925            $id = (int) $id;
2926
2927            if ( $id > 0 ) {
2928
2929                return $this->DAL()->getFieldByID(
2930                    array(
2931                        'id'          => $id,
2932                        'objtype'     => ZBS_TYPE_INVOICE,
2933                        'colname'     => 'zbs_owner',
2934                        'ignoreowner' => true,
2935                    )
2936                );
2937
2938            }
2939
2940            return false;
2941        }
2942
2943        /**
2944         * Returns the first contact associated with an invoice
2945         *
2946         * @param int id quote ID
2947         *
2948         * @return int quote invoice id
2949         */
2950        public function getInvoiceContactID( $id = -1 ) {
2951
2952            global $zbs;
2953
2954            $id = (int) $id;
2955
2956            if ( $id > 0 ) {
2957
2958                $contacts = $this->DAL()->getObjsLinkedToObj(
2959                    array(
2960
2961                        'objtypefrom' => ZBS_TYPE_INVOICE,
2962                        'objtypeto'   => ZBS_TYPE_CONTACT,
2963                        'objfromid'   => $id,
2964                        'count'       => false,
2965
2966                    )
2967                );
2968
2969                if ( is_array( $contacts ) ) {
2970                    foreach ( $contacts as $c ) {
2971
2972                        // first
2973                        return $c['id'];
2974
2975                    }
2976                }
2977            }
2978
2979            return false;
2980        }
2981
2982        /**
2983         * Returns a contact obj assigned to this invoice
2984         *
2985         * @param int id invoice ID
2986         *
2987         * @return int invoice owner id
2988         */
2989        public function getInvoiceContact( $id = -1 ) {
2990
2991            global $zbs;
2992
2993            $id = (int) $id;
2994
2995            if ( $id > 0 ) {
2996
2997                $contacts = $this->DAL()->contacts->getContacts(
2998                    array(
2999
3000                        // link
3001                        'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
3002                        'hasObjIDLinkedTo'   => $resDataLine->ID,
3003
3004                        // query bits
3005                        'perPage'            => -1,
3006                        'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),
3007                    )
3008                );
3009
3010                if ( is_array( $contacts ) && isset( $contacts[0] ) ) {
3011                    return $contacts[0];
3012                }
3013            }
3014
3015            return false;
3016        }
3017
3018        /**
3019         * Returns a company obj assigned to this invoice
3020         *
3021         * @param int id invoice ID
3022         *
3023         * @return int invoice owner id
3024         */
3025        public function getInvoiceCompany( $id = -1 ) {
3026
3027            global $zbs;
3028
3029            $id = (int) $id;
3030
3031            if ( $id > 0 ) {
3032
3033                $companies = $this->DAL()->companies->getCompanies(
3034                    array(
3035
3036                        'hasObjTypeLinkedTo' => ZBS_TYPE_INVOICE,
3037                        'hasObjIDLinkedTo'   => $resDataLine->ID,
3038
3039                        'perPage'            => -1,
3040                        'ignoreowner'        => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_COMPANY ),
3041                    )
3042                );
3043
3044                if ( is_array( $companies ) && isset( $companies[0] ) ) {
3045                    return $companies[0];
3046                }
3047            }
3048
3049            return false;
3050        }
3051
3052        /**
3053         * Returns an status against a invoice
3054         *
3055         * @param int $id invoice ID.
3056         *
3057         * @return string invoice status string
3058         */
3059        public function getInvoiceStatus( $id = -1 ) {
3060
3061            global $zbs;
3062
3063            $id = (int) $id;
3064
3065            if ( $id > 0 ) {
3066
3067                return $this->DAL()->getFieldByID(
3068                    array(
3069                        'id'          => $id,
3070                        'objtype'     => ZBS_TYPE_INVOICE,
3071                        'colname'     => 'zbsi_status',
3072                        'ignoreowner' => true,
3073                    )
3074                );
3075
3076            }
3077
3078            return false;
3079        }
3080
3081        /**
3082         * Returns an SQL query addition which will allow filtering of invoices
3083         * that should be included in "total value" fields, excluding 'deleted' status invoices.
3084         *
3085         * @param string $table_alias_sql - if using a table alias pass that here, e.g. `invoices.`.
3086         * @return array
3087         */
3088        public function get_invoice_status_except_deleted_for_query( $table_alias_sql = '' ) {
3089
3090            $invoice_statuses_except_deleted = array( 'Draft', 'Unpaid', 'Paid', 'Overdue' );
3091
3092            $inv_statuses_str = $this->build_csv( $invoice_statuses_except_deleted );
3093
3094            $query_addition = ' AND ' . $table_alias_sql . 'zbsi_status IN (' . $inv_statuses_str . ')';
3095
3096            return $query_addition;
3097        }
3098
3099        /**
3100         * Returns an hash against a invoice
3101         *
3102         * @param int $id invoice ID.
3103         *
3104         * @return string invoice hash string
3105         */
3106        public function getInvoiceHash( $id = -1 ) {
3107
3108            global $zbs;
3109
3110            $id = (int) $id;
3111
3112            if ( $id > 0 ) {
3113
3114                return $this->DAL()->getFieldByID(
3115                    array(
3116                        'id'          => $id,
3117                        'objtype'     => ZBS_TYPE_INVOICE,
3118                        'colname'     => 'zbsi_hash',
3119                        'ignoreowner' => true,
3120                    )
3121                );
3122
3123            }
3124
3125            return false;
3126        }
3127
3128        /**
3129         * Retrieves outstanding balanace against an invoice, based on transactions assigned to it.
3130         *
3131         * @param int $invoiceID invoice ID.
3132         *
3133         * @return float invoice outstanding balance
3134         */
3135        public function getOutstandingBalance( $invoiceID = -1 ) {
3136
3137            if ( $invoiceID > 0 ) {
3138
3139                $invoice = $this->getInvoice(
3140                    $invoiceID,
3141                    array(
3142
3143                        'withTransactions' => true, // so we can see other partials and check if paid.
3144
3145                    )
3146                );
3147
3148                if ( is_array( $invoice ) ) {
3149
3150                    // get total due
3151                        $invoice_total_value = 0.0;
3152                    if ( isset( $invoice['total'] ) ) {
3153                        $invoice_total_value = (float) $invoice['total'];
3154                        // this one'll be a rolling sum
3155                        $transactions_total_value = 0.0;
3156
3157                        // cycle through trans + calc existing balance
3158                        if ( isset( $invoice['transactions'] ) && is_array( $invoice['transactions'] ) ) {
3159
3160                            // got trans
3161                            foreach ( $invoice['transactions'] as $transaction ) {
3162
3163                                // should we also check for status=completed/succeeded? (leaving for now, will let check all):
3164
3165                                // get amount
3166                                $transaction_amount = 0.0;
3167
3168                                if ( isset( $transaction['total'] ) ) {
3169                                    $transaction_amount = (float) $transaction['total'];
3170
3171                                    if ( $transaction_amount > 0 ) {
3172
3173                                        switch ( $transaction['type'] ) {
3174                                            case __( 'Sale', 'zero-bs-crm' ):
3175                                                // these count as debits against invoice.
3176                                                $transactions_total_value -= $transaction_amount;
3177
3178                                                break;
3179
3180                                            case __( 'Refund', 'zero-bs-crm' ):
3181                                            case __( 'Credit Note', 'zero-bs-crm' ):
3182                                                // these count as credits against invoice.
3183                                                $transactions_total_value += $transaction_amount;
3184
3185                                                break;
3186
3187                                        } // / switch on type (sale/refund)
3188
3189                                    } // / if trans > 0
3190
3191                                } // / if isset
3192
3193                            } // / each trans
3194
3195                            // should now have $transactions_total_value & $invoice_total_value
3196                            // ... so we sum + return.
3197                            return $invoice_total_value + $transactions_total_value;
3198
3199                        } // / if has trans
3200                    } //  if isset invoice total
3201
3202                } // / if retrieved inv
3203
3204            } // / if invoice_id > 0
3205
3206            return false;
3207        }
3208
3209        /**
3210         * remove any non-db fields from the object
3211         * basically takes array like array('owner'=>1,'fname'=>'x','fullname'=>'x')
3212         * and returns array like array('owner'=>1,'fname'=>'x')
3213         * This does so based on the objectModel!
3214         *
3215         * @param array $obj (clean obj)
3216         *
3217         * @return array (db ready arr)
3218         */
3219        private function db_ready_invoice( $obj = false ) {
3220
3221            // use the generic? (override here if necessary)
3222            return $this->db_ready_obj( $obj );
3223        }
3224
3225        /**
3226         * Takes full object and makes a "list view" boiled down version
3227         * Used to generate listview objs
3228         *
3229         * @param array $obj (clean obj)
3230         *
3231         * @return array (listview ready obj)
3232         */
3233        public function listViewObj( $invoice = false, $columnsRequired = array() ) {
3234
3235            if ( is_array( $invoice ) && isset( $invoice['id'] ) ) {
3236
3237                $resArr = $invoice;
3238
3239                // a lot of this is legacy <DAL3 stuff just mapped. def could do with an improvement for efficacy's sake.
3240
3241                // $resArr['id'] = $invoice['id'];
3242                // $resArr['zbsid'] = $invoice['zbsid'];
3243                if ( isset( $invoice['id_override'] ) && $invoice['id_override'] !== null ) {
3244                    $resArr['zbsid'] = $invoice['id_override'];
3245                } else {
3246                    $resArr['zbsid'] = $invoice['id'];
3247                }
3248
3249                // title... I suspect you mean ref?
3250                // WH note: I suspect we mean id_override now.
3251                $resArr['title'] = ''; // if (isset($invoice['name'])) $resArr['title'] = $invoice['name'];
3252                if ( isset( $invoice['id_override'] ) && empty( $resArr['title'] ) ) {
3253                    $resArr['title'] = $invoice['id_override'];
3254                }
3255
3256                #} Convert $contact arr into list-view-digestable 'customer'// & unset contact for leaner data transfer
3257                if ( array_key_exists( 'contact', $invoice ) ) {
3258                    $resArr['customer'] = zeroBSCRM_getSimplyFormattedContact( $invoice['contact'], ( in_array( 'assignedobj', $columnsRequired ) ) );
3259                }
3260
3261                #} Convert $contact arr into list-view-digestable 'customer'// & unset contact for leaner data transfer
3262                if ( array_key_exists( 'company', $invoice ) ) {
3263                    $resArr['company'] = zeroBSCRM_getSimplyFormattedCompany( $invoice['company'], ( in_array( 'assignedobj', $columnsRequired ) ) );
3264                }
3265
3266                // format currency handles if the amount is blank (sends it to 0)
3267                // WH: YES but it doesn't check if isset / stop php notice $resArr['value'] = zeroBSCRM_formatCurrency($invoice['meta']['val']);
3268                $resArr['total'] = zeroBSCRM_formatCurrency( 0 );
3269                if ( isset( $invoice['total'] ) ) {
3270                    $resArr['total'] = zeroBSCRM_formatCurrency( $invoice['total'] );
3271                }
3272
3273                return $resArr;
3274
3275            }
3276
3277            return false;
3278        }
3279
3280        // ===========  /   INVOICE  =======================================================
3281        // ===============================================================================
3282} // / class