HasItemStocks::take()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 3
nc 2
nop 3
1
<?php
2
3
namespace Ronmrcdo\Inventory\Traits;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\DB;
7
use Ronmrcdo\Inventory\Exceptions\NotEnoughStockException;
8
use Ronmrcdo\Inventory\Exceptions\InvalidMovementException;
9
10
trait HasItemStocks
11
{
12
	/**
13
     * Stores the quantity before an update.
14
     *
15
     * @var int|float|string
16
     */
17
    private $beforeQuantity = 0;
18
19
    /**
20
     * Stores the reason for updating / creating a stock.
21
     *
22
     * @var string
23
     */
24
    public $reason = '';
25
26
    /**
27
     * Stores the cost for updating a stock.
28
     *
29
     * @var int|float|string
30
     */
31
	public $cost = 0;
32
	
33
	protected static function bootHasItemStocks()
34
	{
35
		static::creating(function (Model $model) {
36
37
            /*
38
             * Check if a reason has been set, if not
39
             * let's retrieve the default first entry reason
40
             */
41
            if (!$model->reason) {
42
                $model->reason = 'First Item Record; Stock Increase';
43
            }
44
		});
45
		
46
		static::created(function (Model $model) {
47
            $model->postCreate();
48
		});
49
		
50
		static::updating(function (Model $model) {
51
            /*
52
             * Retrieve the original quantity before it was updated,
53
             * so we can create generate an update with it
54
             */
55
            $model->beforeQuantity = $model->getOriginal('quantity');
56
57
            /*
58
             * Check if a reason has been set, if not let's retrieve the default change reason
59
             */
60
            if (!$model->reason) {
61
                $model->reason = 'Stock Adjustment';
62
            }
63
		});
64
		
65
		static::updated(function (Model $model) {
66
            $model->postUpdate();
67
        });
68
	}
69
70
	/**
71
     * Generates a stock movement on the creation of a stock.
72
     */
73
    public function postCreate()
74
    {
75
        if (!$this->getLastMovement()) {
76
            $this->generateStockMovement(0, $this->quantity, $this->reason, $this->cost);
77
        }
78
	}
79
	
80
	/**
81
     * Generates a stock movement after a stock is updated.
82
     */
83
    public function postUpdate()
84
    {
85
        $this->generateStockMovement($this->beforeQuantity, $this->quantity, $this->reason, $this->cost);
86
	}
87
	
88
	/**
89
     * Performs a quantity update. Automatically determining
90
     * depending on the quantity entered if stock is being taken
91
     * or added.
92
     *
93
     * @param int|float|string $quantity
94
     * @param string           $reason
95
     * @param int|float|string $cost
96
     *
97
     * @throws InvalidQuantityException
98
     *
99
     * @return $this
100
     */
101
    public function updateQuantity($quantity, $reason = '', $cost = 0)
102
    {
103
        if ($this->isValidQuantity($quantity)) {
0 ignored issues
show
Bug introduced by
It seems like isValidQuantity() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

103
        if ($this->/** @scrutinizer ignore-call */ isValidQuantity($quantity)) {
Loading history...
104
            return $this->processUpdateQuantityOperation($quantity, $reason, $cost);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->processUpd...antity, $reason, $cost) could also return false which is incompatible with the documented return type Ronmrcdo\Inventory\Traits\HasItemStocks. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
105
        }
106
    }
107
108
    /**
109
     * Removes the specified quantity from the current stock.
110
     *
111
     * @param int|float|string $quantity
112
     * @param string           $reason
113
     * @param int|float|string $cost
114
     *
115
     * @return $this|bool
116
     */
117
    public function remove($quantity, $reason = '', $cost = 0)
118
    {
119
        return $this->take($quantity, $reason, $cost);
120
    }
121
122
    /**
123
     * Processes a 'take' operation on the current stock.
124
     *
125
     * @param int|float|string $quantity
126
     * @param string           $reason
127
     * @param int|float|string $cost
128
     *
129
     * @throws InvalidQuantityException
130
     * @throws NotEnoughStockException
131
     *
132
     * @return $this|bool
133
     */
134
    public function take($quantity, $reason = '', $cost = 0)
135
    {
136
        if ($this->isValidQuantity($quantity) && $this->hasEnoughStock($quantity)) {
137
            return $this->processTakeOperation($quantity, $reason, $cost);
138
        }
139
    }
140
141
    /**
142
     * Alias for put function.
143
     *
144
     * @param int|float|string $quantity
145
     * @param string           $reason
146
     * @param int|float|string $cost
147
     *
148
     * @return $this
149
     */
150
    public function add($quantity, $reason = '', $cost = 0)
151
    {
152
        return $this->put($quantity, $reason, $cost);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->put($quantity, $reason, $cost) could also return false which is incompatible with the documented return type Ronmrcdo\Inventory\Traits\HasItemStocks. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
153
    }
154
155
    /**
156
     * Processes a 'put' operation on the current stock.
157
     *
158
     * @param int|float|string $quantity
159
     * @param string           $reason
160
     * @param int|float|string $cost
161
     *
162
     * @throws InvalidQuantityException
163
     *
164
     * @return $this
165
     */
166
    public function put($quantity, $reason = '', $cost = 0)
167
    {
168
        if ($this->isValidQuantity($quantity)) {
169
            return $this->processPutOperation($quantity, $reason, $cost);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->processPut...antity, $reason, $cost) could also return false which is incompatible with the documented return type Ronmrcdo\Inventory\Traits\HasItemStocks. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
170
        }
171
    }
172
173
    /**
174
     * Moves a stock to the specified location.
175
     *
176
     * @param $location
177
     *
178
     * @return bool
179
     */
180
    public function moveTo($location)
181
    {
182
        $location = $this->getLocation($location);
0 ignored issues
show
Bug introduced by
It seems like getLocation() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
        /** @scrutinizer ignore-call */ 
183
        $location = $this->getLocation($location);
Loading history...
183
184
        return $this->processMoveOperation($location);
185
    }
186
187
    /**
188
     * Rolls back the last movement, or the movement specified. If recursive is set to true,
189
     * it will rollback all movements leading up to the movement specified.
190
     *
191
     * @param mixed $movement
192
     * @param bool  $recursive
193
     *
194
     * @return $this|bool
195
     */
196
    public function rollback($movement = null, $recursive = false)
197
    {
198
        if ($movement) {
199
            return $this->rollbackMovement($movement, $recursive);
200
        } else {
201
            $movement = $this->getLastMovement();
202
203
            if ($movement) {
204
                return $this->processRollbackOperation($movement, $recursive);
205
            }
206
        }
207
208
        return false;
209
    }
210
211
    /**
212
     * Rolls back a specific movement.
213
     *
214
     * @param mixed $movement
215
     * @param bool  $recursive
216
     *
217
     * @throws InvalidMovementException
218
     *
219
     * @return $this|bool
220
     */
221
    public function rollbackMovement($movement, $recursive = false)
222
    {
223
        $movement = $this->getMovement($movement);
224
225
        return $this->processRollbackOperation($movement, $recursive);
226
    }
227
228
    /**
229
     * Returns true if there is enough stock for the specified quantity being taken.
230
     * Throws NotEnoughStockException otherwise.
231
     *
232
     * @param int|float|string $quantity
233
     *
234
     * @throws NotEnoughStockException
235
     *
236
     * @return bool
237
     */
238
    public function hasEnoughStock($quantity = 0)
239
    {
240
        /*
241
         * Using double equals for validation of complete value only, not variable type. For example:
242
         * '20' (string) equals 20 (int)
243
         */
244
        if ($this->quantity == $quantity || $this->quantity > $quantity) {
245
            return true;
246
        }
247
248
        $message = 'Not enough stock. Tried to take '. $quantity.' but only '. $this->quantity .' is available';
249
250
        throw new NotEnoughStockException($message);
251
    }
252
253
    /**
254
     * Returns the last movement on the current stock record.
255
     *
256
     * @return mixed
257
     */
258
    public function getLastMovement()
259
    {
260
        $movement = $this->movements()->orderBy('created_at', 'DESC')->first();
0 ignored issues
show
Bug introduced by
It seems like movements() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

260
        $movement = $this->/** @scrutinizer ignore-call */ movements()->orderBy('created_at', 'DESC')->first();
Loading history...
261
262
        if ($movement) {
263
            return $movement;
264
        }
265
266
        return false;
267
    }
268
269
    /**
270
     * Returns a movement depending on the specified argument. If an object is supplied, it is checked if it
271
     * is an instance of an eloquent model. If a numeric value is entered, it is retrieved by it's ID.
272
     *
273
     * @param mixed $movement
274
     *
275
     * @throws InvalidMovementException
276
     *
277
     * @return mixed
278
     */
279
    public function getMovement($movement)
280
    {
281
        if ($this->isModel($movement)) {
0 ignored issues
show
Bug introduced by
It seems like isModel() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

281
        if ($this->/** @scrutinizer ignore-call */ isModel($movement)) {
Loading history...
282
            return $movement;
283
        } elseif (is_numeric($movement)) {
284
            return $this->getMovementById($movement);
285
        } else {
286
            $message = 'Movement '. $movement .' is invalid';
287
288
            throw new InvalidMovementException($message);
289
        }
290
    }
291
292
    /**
293
     * Creates and returns a new un-saved stock transaction
294
     * instance with the current stock ID attached.
295
     *
296
     * @param string $name
297
     *
298
     * @return \Illuminate\Database\Eloquent\Model
299
     */
300
    public function newTransaction($name = '')
301
    {
302
        $transaction = $this->transactions()->getRelated();
0 ignored issues
show
Bug introduced by
It seems like transactions() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

302
        $transaction = $this->/** @scrutinizer ignore-call */ transactions()->getRelated();
Loading history...
303
304
        /*
305
         * Set the transaction attributes so they don't
306
         * need to be set manually
307
         */
308
        $transaction->stock_id = $this->getKey();
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

308
        /** @scrutinizer ignore-call */ 
309
        $transaction->stock_id = $this->getKey();
Loading history...
309
        $transaction->name = $name;
310
311
        return $transaction;
312
    }
313
314
    /**
315
     * Retrieves a movement by the specified ID.
316
     *
317
     * @param int|string $id
318
     *
319
     * @return mixed
320
     */
321
    private function getMovementById($id)
322
    {
323
        return $this->movements()->find($id);
324
    }
325
326
    /**
327
     * Processes a quantity update operation.
328
     *
329
     * @param int|float|string $quantity
330
     * @param string           $reason
331
     * @param int|float|string $cost
332
     *
333
     * @return $this
334
     */
335
    private function processUpdateQuantityOperation($quantity, $reason = '', $cost = 0)
336
    {
337
        if ($quantity > $this->quantity) {
338
            $putting = $quantity - $this->quantity;
339
340
            return $this->put($putting, $reason, $cost);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->put($putting, $reason, $cost) could also return false which is incompatible with the documented return type Ronmrcdo\Inventory\Traits\HasItemStocks. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
341
        } else {
342
            $taking = $this->quantity - $quantity;
343
344
            return $this->take($taking, $reason, $cost);
345
        }
346
    }
347
348
    /**
349
     * Processes removing quantity from the current stock.
350
     *
351
     * @param int|float|string $taking
352
     * @param string           $reason
353
     * @param int|float|string $cost
354
     *
355
     * @return $this|bool
356
     */
357
    private function processTakeOperation($taking, $reason = '', $cost = 0)
358
    {
359
        $left = $this->quantity - $taking;
360
361
        /*
362
         * If the updated total and the beginning total are the same, we'll check if
363
         * duplicate movements are allowed. We'll return the current record if
364
         * they aren't.
365
         */
366
        if ($left == $this->quantity && !$this->allowDuplicateMovementsEnabled()) {
367
            return $this;
368
        }
369
370
        $this->quantity = $left;
0 ignored issues
show
Bug Best Practice introduced by
The property quantity does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
371
372
        $this->setReason($reason);
373
374
        $this->setCost($cost);
375
376
        DB::beginTransaction();
377
378
        try {
379
            if ($this->save()) {
0 ignored issues
show
Bug introduced by
It seems like save() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

379
            if ($this->/** @scrutinizer ignore-call */ save()) {
Loading history...
380
381
                $this->fireModelEvent('inventory.stock.taken', [
0 ignored issues
show
Bug introduced by
It seems like fireModelEvent() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

381
                $this->/** @scrutinizer ignore-call */ 
382
                       fireModelEvent('inventory.stock.taken', [
Loading history...
382
                    'stock' => $this,
383
                ]);
384
385
                return $this;
386
            }
387
        } catch (\Exception $e) {
388
            DB::rollBack();
389
        }
390
391
        return false;
392
    }
393
394
    /**
395
     * Processes adding quantity to current stock.
396
     *
397
     * @param int|float|string $putting
398
     * @param string           $reason
399
     * @param int|float|string $cost
400
     *
401
     * @return $this|bool
402
     */
403
    private function processPutOperation($putting, $reason = '', $cost = 0)
404
    {
405
        $before = $this->quantity;
406
407
        $total = $putting + $before;
408
409
        /*
410
         * If the updated total and the beginning total are the same,
411
         * we'll check if duplicate movements are allowed
412
         */
413
        if ($total == $this->quantity && !$this->allowDuplicateMovementsEnabled()) {
414
            return $this;
415
        }
416
417
        $this->quantity = $total;
0 ignored issues
show
Bug Best Practice introduced by
The property quantity does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
418
419
        $this->setReason($reason);
420
421
        $this->setCost($cost);
422
423
        DB::beginTransaction();
424
425
        try {
426
            if ($this->save()) {
427
                DB::commit();
428
429
                $this->fireModelEvent('inventory.stock.added', [
430
                    'stock' => $this,
431
                ]);
432
433
                return $this;
434
            }
435
        } catch (\Exception $e) {
436
            DB::rollBack();
437
        }
438
439
        return false;
440
    }
441
442
    /**
443
     * Processes the stock moving from it's current
444
     * location, to the specified location.
445
     *
446
     * @param mixed $location
447
     *
448
     * @return bool
449
     */
450
    private function processMoveOperation(Model $location)
451
    {
452
        $this->location_id = $location->getKey();
0 ignored issues
show
Bug Best Practice introduced by
The property location_id does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
453
454
        DB::beginTransaction();
455
456
        try {
457
            if ($this->save()) {
458
                
459
460
                $this->fireModelEvent('inventory.stock.moved', [
461
                    'stock' => $this,
462
                ]);
463
				
464
				DB::commit();
465
                return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Ronmrcdo\Inventory\Traits\HasItemStocks which is incompatible with the documented return type boolean.
Loading history...
466
            }
467
        } catch (\Exception $e) {
468
            DB::rollBack();
469
        }
470
471
        return false;
472
    }
473
474
    /**
475
     * Processes a single rollback operation.
476
     *
477
     * @param mixed $movement
478
     * @param bool  $recursive
479
     *
480
     * @return $this|bool
481
     */
482
    private function processRollbackOperation(Model $movement, $recursive = false)
483
    {
484
        if ($recursive) {
485
            return $this->processRecursiveRollbackOperation($movement);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->processRec...ackOperation($movement) returns the type array which is incompatible with the documented return type Ronmrcdo\Inventory\Traits\HasItemStocks|boolean.
Loading history...
486
        }
487
488
        $this->quantity = $movement->before;
0 ignored issues
show
Bug Best Practice introduced by
The property quantity does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
489
490
        $reason = 'Rolled back to movement ID: '. $movement->getOriginal('id') .' on '. $movement->getOriginal('created_at');
0 ignored issues
show
Bug introduced by
Are you sure $movement->getOriginal('created_at') of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

490
        $reason = 'Rolled back to movement ID: '. $movement->getOriginal('id') .' on '. /** @scrutinizer ignore-type */ $movement->getOriginal('created_at');
Loading history...
Bug introduced by
Are you sure $movement->getOriginal('id') of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

490
        $reason = 'Rolled back to movement ID: '. /** @scrutinizer ignore-type */ $movement->getOriginal('id') .' on '. $movement->getOriginal('created_at');
Loading history...
491
492
        $this->setReason($reason);
493
494
        if ($this->rollbackCostEnabled()) {
495
            $this->setCost($movement->cost);
0 ignored issues
show
Bug introduced by
It seems like $movement->cost can also be of type Illuminate\Database\Eloq...uent\Relations\Relation and Illuminate\Database\Eloquent\Relations\Relation; however, parameter $cost of Ronmrcdo\Inventory\Traits\HasItemStocks::setCost() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

495
            $this->setCost(/** @scrutinizer ignore-type */ $movement->cost);
Loading history...
496
497
            $this->reverseCost();
498
        }
499
500
        DB::beginTransaction();
501
502
        try {
503
            if ($this->save()) {
504
                DB::commit();
505
506
                $this->fireModelEvent('inventory.stock.rollback', [
507
                    'stock' => $this,
508
                ]);
509
510
                return $this;
511
            }
512
        } catch (\Exception $e) {
513
            DB::rollBack();
514
        }
515
516
        return false;
517
    }
518
519
    /**
520
     * Processes a recursive rollback operation.
521
     *
522
     * @param mixed $movement
523
     *
524
     * @return array
525
     */
526
    private function processRecursiveRollbackOperation(Model $movement)
527
    {
528
        /*
529
         * Retrieve movements that were created after
530
         * the specified movement, and order them descending
531
         */
532
        $movements = $this
533
            ->movements()
534
            ->where('created_at', '>=', $movement->getOriginal('created_at'))
535
            ->orderBy('created_at', 'DESC')
536
            ->get();
537
538
        $rollbacks = [];
539
540
        if ($movements->count() > 0) {
541
            foreach ($movements as $movement) {
542
                $rollbacks = $this->processRollbackOperation($movement);
543
            }
544
        }
545
546
        return $rollbacks;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $rollbacks also could return the type Ronmrcdo\Inventory\Traits\HasItemStocks|false which is incompatible with the documented return type array.
Loading history...
547
    }
548
549
    /**
550
     * Creates a new stock movement record.
551
     *
552
     * @param int|float|string $before
553
     * @param int|float|string $after
554
     * @param string           $reason
555
     * @param int|float|string $cost
556
     *
557
     * @return \Illuminate\Database\Eloquent\Model
558
     */
559
    private function generateStockMovement($before, $after, $reason = '', $cost = 0)
560
    {
561
        $insert = [
562
            'stock_id' => $this->getKey(),
563
            'before' => $before,
564
            'after' => $after,
565
            'reason' => $reason,
566
            'cost' => $cost,
567
        ];
568
569
        return $this->movements()->create($insert);
570
    }
571
572
    /**
573
     * Sets the cost attribute.
574
     *
575
     * @param int|float|string $cost
576
     */
577
    private function setCost($cost = 0)
578
    {
579
        $this->cost = $cost;
580
    }
581
582
    /**
583
     * Reverses the cost of a movement.
584
     */
585
    private function reverseCost()
586
    {
587
        if ($this->isPositive($this->cost)) {
0 ignored issues
show
Bug introduced by
It seems like isPositive() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

587
        if ($this->/** @scrutinizer ignore-call */ isPositive($this->cost)) {
Loading history...
588
            $this->setCost(-abs($this->cost));
589
        } else {
590
            $this->setCost(abs($this->cost));
591
        }
592
    }
593
594
    /**
595
     * Sets the reason attribute.
596
     *
597
     * @param string $reason
598
     */
599
    private function setReason($reason = '')
600
    {
601
        $this->reason = $reason;
602
    }
603
604
    /**
605
     * Returns true/false from the configuration file determining
606
     * whether or not stock movements can have the same before and after
607
     * quantities.
608
     *
609
     * @return bool
610
     */
611
    private function allowDuplicateMovementsEnabled()
612
    {
613
        return false;
614
    }
615
616
    /**
617
     * Returns true/false from the configuration file determining
618
     * whether or not to rollback costs when a rollback occurs on
619
     * a stock.
620
     *
621
     * @return bool
622
     */
623
    private function rollbackCostEnabled()
624
    {
625
        return true;
626
    }
627
628
}
629