madbob/GASdottoNG

View on GitHub
code/app/Product.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;

use App\Models\Concerns\ModifiableTrait;
use App\Models\Concerns\Priceable;
use App\Models\Concerns\ProductConcept;
use App\Models\Concerns\TracksUpdater;
use App\Events\VariantChanged;
use App\Events\SluggableCreating;

class Product extends Model
{
    use HasFactory, SoftDeletes, ProductConcept, TracksUpdater, Priceable, ModifiableTrait, GASModel, SluggableID, Cachable;

    public $incrementing = false;
    protected $keyType = 'string';

    protected $dispatchesEvents = [
        'creating' => SluggableCreating::class,
    ];

    protected $casts = [
        'active' => 'boolean',
    ];

    protected static function boot()
    {
        parent::boot();
        static::initTrackingEvents();
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function measure(): BelongsTo
    {
        return $this->belongsTo(Measure::class);
    }

    public function supplier(): BelongsTo
    {
        return $this->belongsTo(Supplier::class);
    }

    public function orders(): BelongsToMany
    {
        return $this->belongsToMany(Order::class);
    }

    public function variants(): HasMany
    {
        return $this->hasMany(Variant::class)->with('values')->orderBy('name', 'asc');
    }

    public function vat_rate(): BelongsTo
    {
        return $this->belongsTo(VatRate::class);
    }

    public function scopeSorted($query)
    {
        if (currentAbsoluteGas()->manual_products_sorting) {
            $query->orderBy('products.sorting')->orderBy('products.name');
        }
        else {
            $query->orderBy('products.name');
        }
    }

    public function getSlugID()
    {
        return sprintf('%s::%s', $this->supplier_id, Str::slug($this->name));
    }

    public function getPictureUrlAttribute()
    {
        if (empty($this->picture))
            return '';
        else
            return url('products/picture/' . $this->id);
    }

    public function getFixedPackageSizeAttribute()
    {
        if ($this->portion_quantity <= 0) {
            return $this->package_size;
        }
        else {
            return round($this->portion_quantity * $this->package_size, 2);
        }
    }

    public function getVariantCombosAttribute()
    {
        return $this->innerCache('variant_combos', function($obj) {
            return app("model-cache")->runDisabled(function() use ($obj) {
                $ret = VariantCombo::whereHas('values', function($query) use ($obj) {
                    $query->whereHas('variant', function($query) use ($obj) {
                        $query->where('product_id', $obj->id);
                    });
                })->with(['values', 'values.variant'])->get();

                /*
                    Per scrupolo qui faccio un controllo: se il prodotto ha delle
                    varianti ma nessuna combo, ne forzo qui la rigenerazione
                */
                if ($ret->isEmpty() && $this->variants->count() != 0) {
                    foreach($this->variants as $variant) {
                        VariantChanged::dispatch($variant);
                    }

                    return $this->getVariantCombosAttribute();
                }
                else {
                    /*
                        Una volta ottenuto l'elenco delle combo, setto in modo
                        esplicito la relazione con il prodotto corrente.
                        Questo perché VariantCombo dipende da questa relazione per
                        determinare quale sia il suo stesso prodotto, il quale viene
                        usato sia per formattarne il nome che per determinarne il
                        prezzo (se il prodotto corrente è nel contesto di un ordine)
                    */

                    foreach($ret as $vc) {
                        foreach($vc->values as $val) {
                            $val->variant->setRelation('product', $obj);
                        }
                    }

                    return $ret;
                }
            });
        });
    }

    public function getSortedVariantCombosAttribute()
    {
        return $this->variant_combos->where('active', true)->sortBy(function($combo, $key) {
            return $combo->values->pluck('value')->join(' ');
        }, SORT_NATURAL);
    }

    public function getCategoryNameAttribute()
    {
        $cat = $this->category;
        if ($cat)
            return $cat->name;
        else
            return '';
    }

    public function bookingsInOrder($order)
    {
        $id = $this->id;

        return Booking::where('order_id', '=', $order->id)->whereHas('products', function ($query) use ($id) {
            $query->where('product_id', '=', $id);
        })->get();
    }

    public function printablePrice($variant = null)
    {
        $price = $this->getPrice(false);

        if ($this->variants->count() != 0) {
            if (is_null($variant)) {
                /*
                    È rilevante l'ordinamento alfabetico dei valori, soprattutto
                    quando nessuna variante è selezionata di default: essendo
                    preso sempre il primo valore, bisogna accertarsi che il
                    primo sia sempre lo stesso
                */
                $variant = $this->sortedVariantCombos->first();
            }

            if ($variant) {
                $price = $variant->getPrice();
            }
        }

        $currency = defaultCurrency()->symbol;

        /*
            Qui uso sempre il nome assoluto dell'unità di misura, in quanto
            anche per i prodotti con pezzatura il prezzo è sempre riferito al
            chilo
        */
        $str = sprintf('%.02f %s / %s', $price, $currency, $this->measure->name);

        return $str;
    }

    public function printableMeasure($verbose = false)
    {
        if ($this->portion_quantity != 0) {
            if ($verbose) {
                return sprintf('Pezzi da %.02f %s', $this->portion_quantity, $this->measure->name);
            }
            else {
                return sprintf('%.02f %s', $this->portion_quantity, $this->measure->name);
            }
        }
        else {
            $m = $this->measure;
            return $m->name ?? '';
        }
    }

    public function printableDetails($order)
    {
        $details = [];

        $constraints = systemParameters('Constraints');
        foreach($constraints as $constraint) {
            $string = $constraint->printable($this, $order);
            if ($string) {
                $details[] = $string;
            }
        }

        return implode(', ', $details);
    }

    /*
        Questa funzione determina se posso aggregare le quantità per lo stesso
        prodotto all'interno della stessa prenotazione, in presenza di amici o
        varianti con la stessa combinazione.
        Ci sono casi in cui voglio un unico prodotto prenotato, con una unica
        quantità, e casi in cui per ogni immissione voglio una quantità separata
        (e.g. la carne venduta a pacchi da N etti: può essere sempre la stessa
        carne, ma ne voglio pacchi diversi ciascuno col suo peso)
    */
    public function canAggregateQuantities()
    {
        $hub = App::make('AggregationSwitch');

        if ($hub->isEnforced()) {
            return true;
        }
        else {
            return ($this->portion_quantity == 0 && $this->measure->discrete == false && $this->variants->count() != 0) == false;
        }
    }

    public function hasWarningWithinOrder($summary)
    {
        if (isset($summary->products[$this->id])) {
            $quantity = $summary->products[$this->id]->quantity;

            if ($quantity != 0) {
                $has_warning = $this->package_size != 0 && round(fmod($quantity, $this->fixed_package_size)) != 0;
                if ($has_warning) {
                    return true;
                }

                $has_warning = $this->global_min != 0 && $quantity < $this->global_min;
                if ($has_warning) {
                    return true;
                }
            }
        }

        return false;
    }

    public function comparePrices($other)
    {
        if ($this->getPrice(false) != $other->getPrice(false)) {
            return false;
        }

        foreach($other->variant_combos as $vc) {
            $ovc = $this->variant_combos->firstWhere('id', $vc->id);
            if ($ovc) {
                if ($ovc->getPrice(false) != $vc->getPrice(false)) {
                    return false;
                }
            }
            else {
                if ($vc->price_offset != 0) {
                    return false;
                }
            }
        }

        return true;
    }

    public static function displayColumns()
    {
        $ret = [];

        $gas = currentAbsoluteGas();
        if ($gas->manual_products_sorting) {
            $ret = [
                'sorting' => (object) [
                    'label' => _i('Ordinamento'),
                    'help' => _i("Ordinamento del prodotto"),
                    'width' => 5
                ],
            ];
        }

        $ret = $ret + [
            'selection' => (object) [
                'label' => _i('Selezione'),
                'help' => _i("Per selezionare il prodotto e compiere operazioni di gruppo"),
                'width' => 5
            ],
            'name' => (object) [
                'label' => _i('Nome'),
                'help' => _i('Nome del prodotto'),
                'width' => 20
            ],
            'category' => (object) [
                'label' => _i('Categoria'),
                'help' => _i('Categoria del prodotto'),
                'width' => 15
            ],
            'measure' => (object) [
                'label' => _i('Unità di Misura'),
                'help' => _i('Unità di misura del prodotto'),
                'width' => 15
            ],
            'price' => (object) [
                'label' => _i('Prezzo Unitario'),
                'help' => _i('Prezzo Unitario del prodotto'),
                'width' => 10
            ],
            'max_available' => (object) [
                'label' => _i('Disponibile'),
                'help' => _i('Quantità massima di prodotto che complessivamente può essere prenotata in un ordine'),
                'width' => 10
            ],
            'active' => (object) [
                'label' => _i('Ordinabile'),
                'help' => _i("Indica se il prodotto potrà essere ordinato o meno all'interno dei nuovi ordini per il fornitore"),
                'width' => 5
            ],
        ];

        return $ret;
    }

    /************************************************************** Priceable */

    public function realPrice($rectify)
    {
        $price = $this->price;

        /*
            Qui si legge l'eventuale attributo "prices" che viene recuperato
            accedendo nella relazione products() di un Order
        */
        if (isset($this->pivot->prices)) {
            $prices = json_decode($this->pivot->prices);
            if ($prices) {
                $price = $prices->unit_price;
            }
        }

        if ($rectify && $this->portion_quantity != 0) {
            $price = $price * $this->portion_quantity;
        }

        return (float) $price;
    }

    /********************************************************* ProductConcept */

    public function getConceptID()
    {
        return $this->id;
    }
}