providerService = $providerService; $this->unitService = $unitService; $this->productService = $productService; $this->dateService = $dateService; $this->currency = $currency; $this->barcodeService = $barcodeService; } /** * get a single procurement * or retrieve a list of procurement * * @param int procurement id * @return Collection|Procurement */ public function get( $id = null ) { if ( $id !== null ) { $provider = Procurement::find( $id ); if ( ! $provider instanceof Procurement ) { throw new Exception( __( 'Unable to find the requested procurement using the provided identifier.' ) ); } return $provider; } return Procurement::get(); } public function procurementName() { $lastProcurement = Procurement::orderBy( 'id', 'desc' )->first(); if ( $lastProcurement instanceof Procurement ) { $number = str_pad( $lastProcurement->id + 1, 5, '0', STR_PAD_LEFT ); } else { $number = str_pad( 1, 5, '0', STR_PAD_LEFT ); } return sprintf( __( '%s' ), $number ); } /** * create a procurement * using the provided informations * * @param array procurement data * @return array|Exception */ public function create( $data ) { return DB::transaction( fn() => $this->persistProcurement( $data ), attempts: 3 ); } private function persistProcurement( $data ) { extract( $data ); /** * try to find the provider * or return an error */ $provider = $this->providerService->get( $data[ 'general' ][ 'provider_id' ] ); if ( ! $provider instanceof Provider ) { throw new Exception( __( 'Unable to find the assigned provider.' ) ); } /** * We'll create a new instance * of the procurement * * @param Procurement */ $procurement = new Procurement; /** * we'll make sure to trigger some event before * performing some change on the procurement */ event( new ProcurementBeforeCreateEvent( $procurement, $data ) ); /** * We don't want the event ProcurementBeforeCreateEvent * and ProcurementAfterCreateEvent to trigger while saving */ Procurement::withoutEvents( function () use ( $procurement, $data ) { $procurement->name = $data[ 'name' ] ?: $this->procurementName(); foreach ( $data[ 'general' ] as $field => $value ) { $procurement->$field = $value; } if ( ! empty( $procurement->created_at ) || ! empty( $procurement->updated_at ) ) { $procurement->timestamps = false; } $procurement->author_id = Auth::id(); $procurement->cost = 0; $procurement->save(); } ); /** * Let's save the product that are procured * This doesn't affect the stock but only store the product */ if ( $data[ 'products' ] ) { $this->saveProducts( $procurement, collect( $data[ 'products' ] ) ); } /** * We can now safely trigger the event here * that will ensure correct computing */ event( new ProcurementAfterCreateEvent( $procurement ) ); return [ 'status' => 'success', 'message' => __( 'The procurement has been created.' ), 'data' => [ 'products' => $procurement->products, 'procurement' => $procurement, ], ]; } /** * Editing a specific procurement using the provided informations * * @param int procurement id * @param array data to update * @return array */ public function edit( $id, $data ) { return DB::transaction( fn() => $this->persistEditedProcurement( $id, $data ), attempts: 3 ); } private function persistEditedProcurement( $id, $data ) { /** * @param array $general * @param string $name * @param array $products */ extract( $data ); /** * try to find the provider * or return an error */ $provider = $this->providerService->get( $data[ 'general' ][ 'provider_id' ] ); if ( ! $provider instanceof Provider ) { throw new Exception( __( 'Unable to find the assigned provider.' ) ); } $procurement = Procurement::findOrFail( $id ); /** * we'll make sure to trigger some event before * performing some change on the procurement */ event( new ProcurementBeforeUpdateEvent( $procurement ) ); /** * We won't dispatch the even while savin the procurement * however we'll do that once the product has been stored. */ Procurement::withoutEvents( function () use ( $data, $procurement ) { if ( $procurement->delivery_status === 'stocked' ) { throw new Exception( __( 'Unable to edit a procurement that has already been stocked. Please consider performing and stock adjustment.' ) ); } $procurement->name = $data[ 'name' ]; foreach ( $data[ 'general' ] as $field => $value ) { $procurement->$field = $value; } if ( ! empty( $procurement->created_at ) || ! empty( $procurement->updated_at ) ) { $procurement->timestamps = false; } $procurement->author_id = Auth::id(); $procurement->cost = 0; $procurement->save(); } ); /** * We can now safely save * the procurement products */ if ( $data[ 'products' ] ) { $this->saveProducts( $procurement, collect( $data[ 'products' ] ) ); } /** * we want to dispatch the event * only when the product has been created */ event( new ProcurementAfterUpdateEvent( $procurement ) ); return [ 'status' => 'success', 'message' => __( 'The provider has been edited.' ), 'data' => compact( 'procurement' ), ]; } /** * delete a specific procurement * using the provided id * * @param int procurement id * @return void */ public function delete( $id ) { $procurement = Procurement::find( $id ); if ( ! $procurement instanceof Procurement ) { throw new Exception( 'Unable to find the requested procurement using the provided id.' ); } $procurement->delete(); return [ 'status' => 'success', 'message' => __( 'The procurement has been deleted.' ), ]; } /** * Attempt a product stock removal * if the procurement has been stocked * * @throws NotAllowedException */ public function attemptProductsStockRemoval( Procurement $procurement ): void { if ( $procurement->delivery_status === 'stocked' ) { $procurement->products->each( function ( ProcurementProduct $procurementProduct ) { /** * We'll handle products that was converted a bit * differently to ensure converted product inventory is taken in account. */ if ( empty( $procurementProduct->convert_unit_id ) ) { $unitQuantity = ProductUnitQuantity::withProduct( $procurementProduct->product_id ) ->withUnit( $procurementProduct->unit_id ) ->first(); $quantity = $procurementProduct->quantity; $unitName = $procurementProduct->unit->name; } else { $fromUnit = $procurementProduct->unit; $toUnit = Unit::find( $procurementProduct->convert_unit_id ); $quantity = $this->unitService->getConvertedQuantity( from: $fromUnit, to: $toUnit, quantity: $procurementProduct->quantity ); $unitName = $toUnit->name; $unitQuantity = ProductUnitQuantity::withProduct( $procurementProduct->product_id ) ->withUnit( $toUnit->id ) ->first(); } if ( $unitQuantity instanceof ProductUnitQuantity ) { if ( floatval( $unitQuantity->quantity ) - floatval( $quantity ) < 0 ) { throw new NotAllowedException( sprintf( __( 'Unable to delete the procurement as there is not enough stock remaining for "%s" on unit "%s". This likely means the stock count has changed either with a sale, adjustment after the procurement has been stocked.' ), $procurementProduct->product->name, $unitName ) ); } } } ); } } /** * This will delete product available on a procurement * and dispatch some events before and after that occurs. */ public function deleteProcurementProducts( Procurement $procurement ): void { $procurement->products->each( function ( ProcurementProduct $product ) use ( $procurement ) { $this->deleteProduct( $product, $procurement ); } ); } /** * This helps to compute the unit value and the total cost * of a procurement product. It return various value as an array of * the product updated along with an array of errors. */ private function __computeProcurementProductValues( array $data ) { /** * @var ProcurementProduct $procurementProduct * @var $storeUnitReference * @var Procurement $procurement * @var $itemsToSave * @var $item */ extract( $data, EXTR_REFS ); if ( $item->purchase_unit_type === 'unit' ) { extract( $this->__procureForSingleUnit( compact( 'procurementProduct', 'storedUnitReference', 'itemsToSave', 'item' ) ) ); } elseif ( $item->purchase_unit_type === 'unit-group' ) { if ( ! isset( $procurementProduct->unit_id ) ) { /** * this is made to ensure * we have a self explanatory error, * that describe why a product couldn't be processed */ $keys = array_keys( (array) $procurementProduct ); foreach ( $keys as $key ) { if ( in_array( $key, [ 'id', 'sku', 'barcode' ] ) ) { $argument = $key; $identifier = $procurementProduct->$key; break; } } $errors[] = [ 'status' => 'error', 'message' => sprintf( __( 'Unable to have a unit group id for the product using the reference "%s" as "%s"' ), $identifier, $argument ), ]; } try { extract( $this->__procureForUnitGroup( compact( 'procurementProduct', 'storedunitReference', 'itemsToSave', 'item' ) ) ); } catch ( Exception $exception ) { $errors[] = [ 'status' => 'error', 'message' => $exception->getMessage(), 'data' => [ 'product' => collect( $item )->only( [ 'id', 'name', 'sku', 'barcode' ] ), ], ]; } } return $data; } /** * This only save the product * but doesn't affect the stock. */ public function saveProducts( Procurement $procurement, Collection $products ) { /** * We'll just make sure to have a reference * of all the product that has been procured. */ $procuredProducts = $products->map( function ( $procuredProduct ) use ( $procurement ) { $product = Product::find( $procuredProduct[ 'product_id' ] ); if ( ! $product instanceof Product ) { throw new Exception( sprintf( __( 'Unable to find the product using the provided id "%s"' ), $procuredProduct[ 'product_id' ] ) ); } if ( $product->stock_management === 'disabled' ) { throw new Exception( sprintf( __( 'Unable to procure the product "%s" as the stock management is disabled.' ), $product->name ) ); } if ( $product->type === 'grouped' ) { throw new Exception( sprintf( __( 'Unable to procure the product "%s" as it is a grouped product.' ), $product->name ) ); } /** * as the id might not always be provided * We'll find some record having an id set to 0 * as not result will pop, that will create a new instance. */ $procurementProduct = ProcurementProduct::find( $procuredProduct[ 'id' ] ?? 0 ); if ( ! $procurementProduct instanceof ProcurementProduct ) { $procurementProduct = new ProcurementProduct; } /** * @todo these value might also * be calculated automatically. */ $procurementProduct->name = $product->name; $procurementProduct->gross_purchase_price = $procuredProduct[ 'gross_purchase_price' ]; $procurementProduct->net_purchase_price = $procuredProduct[ 'net_purchase_price' ]; $procurementProduct->procurement_id = $procurement->id; $procurementProduct->product_id = $procuredProduct[ 'product_id' ]; $procurementProduct->purchase_price = $procuredProduct[ 'purchase_price' ]; $procurementProduct->quantity = $procuredProduct[ 'quantity' ]; $procurementProduct->available_quantity = $procuredProduct[ 'quantity' ]; $procurementProduct->tax_group_id = $procuredProduct[ 'tax_group_id' ] ?? 0; $procurementProduct->tax_type = $procuredProduct[ 'tax_type' ]; $procurementProduct->tax_value = $procuredProduct[ 'tax_value' ]; $procurementProduct->expiration_date = $procuredProduct[ 'expiration_date' ] ?? null; $procurementProduct->total_purchase_price = $procuredProduct[ 'total_purchase_price' ]; $procurementProduct->convert_unit_id = $procuredProduct[ 'convert_unit_id' ] ?? null; $procurementProduct->unit_id = $procuredProduct[ 'unit_id' ]; $procurementProduct->author_id = Auth::id(); $procurementProduct->save(); $procurementProduct->barcode = str_pad( $product->barcode, 5, '0', STR_PAD_LEFT ) . '-' . str_pad( $procurementProduct->unit_id, 3, '0', STR_PAD_LEFT ) . '-' . str_pad( $procurementProduct->id, 3, '0', STR_PAD_LEFT ); $procurementProduct->save(); event( new ProcurementAfterSaveProductEvent( $procurement, $procurementProduct, $procuredProduct ) ); return $procurementProduct; } ); return $procuredProducts; } /** * prepare the procurement entry. */ private function __procureForUnitGroup( array $data ) { /** * @var $storeUnitReference * @var ProcurementProduct $procurementProduct * @var $storedBase * @var $item */ extract( $data ); if ( empty( $stored = @$storedUnitReference[ $procurementProduct->unit_id ] ) ) { $unit = $this->unitService->get( $procurementProduct->unit_id ); $group = $this->unitService->getGroups( $item->purchase_unit_id ); // which should retrieve the group $base = $unit->base_unit ? $unit : $this->unitService->getBaseUnit( $group ); $base_quantity = $this->unitService->computeBaseUnit( $unit, $base, $procurementProduct->quantity ); $storedBase[ $procurementProduct->unit_id ] = compact( 'base', 'unit', 'group' ); } else { extract( $stored ); $base_quantity = $this->unitService->computeBaseUnit( $unit, $base, $procurementProduct->quantity ); } /** * let's check if the unit assigned * during the purchase is a sub unit of the * unit assigned to the item. */ if ( $group->id !== $item->purchase_unit_id ) { throw new Exception( sprintf( __( 'The unit used for the product %s doesn\'t belongs to the Unit Group assigned to the item' ), $item->name ) ); } $itemData = [ 'product_id' => $item->id, 'unit_id' => $procurementProduct->unit_id, 'base_quantity' => $base_quantity, 'quantity' => $procurementProduct->quantity, 'purchase_price' => $this->currency->value( $procurementProduct->purchase_price )->get(), 'total_purchase_price' => $this->currency->value( $procurementProduct->purchase_price )->multiplyBy( $procurementProduct->quantity )->get(), 'author_id' => Auth::id(), 'name' => $item->name, ]; $itemsToSave[] = $itemData; return compact( 'itemsToSave', 'storedUnitReference' ); } private function __procureForSingleUnit( $data ) { extract( $data ); /** * if the purchase unit id hasn't already been * recorded, then let's save it */ if ( empty( $stored = @$storedUnitReference[ $item->purchase_unit_id ] ) ) { $unit = $this->unitService->get( $item->purchase_unit_id ); $group = $unit->group; $base = $unit->base_unit ? $unit : $this->unitService->getBaseUnit( $group ); $base_quantity = $this->unitService->computeBaseUnit( $unit, $base, $procurementProduct->quantity ); $storedUnitReference[ $item->purchase_unit_id ] = compact( 'base', 'unit' ); } else { extract( $stored ); $base_quantity = $this->unitService->computeBaseUnit( $unit, $base, $procurementProduct->quantity ); } $itemData = [ 'product_id' => $item->id, 'unit_id' => $item->purchase_unit_id, 'base_quantity' => $base_quantity, 'quantity' => $procurementProduct->quantity, 'purchase_price' => $this->currency->value( $procurementProduct->purchase_price )->get(), 'total_price' => $this->currency->value( $procurementProduct->purchase_price )->multiplyBy( $procurementProduct->quantity )->get(), 'author_id' => Auth::id(), 'name' => $item->name, ]; $itemsToSave[] = $itemData; return compact( 'itemsToSave', 'storedUnitReference' ); } /** * save a defined procurement products * * @param int procurement id * @param array items * @return array; */ public function saveProcurementProducts( $procurement_id, $items ) { $procuredItems = []; foreach ( $items as $item ) { $product = new ProcurementProduct; foreach ( $item as $field => $value ) { $product->$field = $value; } $product->author_id = Auth::id(); $product->procurement_id = $procurement_id; $product->save(); $procuredItems[] = $product->toArray(); } return [ 'status' => 'success', 'message' => __( 'The operation has completed.' ), 'data' => [ 'success' => $procuredItems, ], ]; } /** * refresh a procurement * by counting the total items & value * * @param Procurement $provided procurement * @return array */ public function refresh( Procurement $procurement ) { /** * @var ProductService */ $productService = app()->make( ProductService::class ); Procurement::withoutEvents( function () use ( $procurement, $productService ) { /** * Let's loop all procured produt * and get unit quantity if that exists * otherwise we'll create a new one. */ $purchases = $procurement ->products() ->get() ->map( function ( $procurementProduct ) use ( $productService ) { $unitPrice = 0; $unit = $productService->getUnitQuantity( $procurementProduct->product_id, $procurementProduct->unit_id ); if ( $unit instanceof ProductUnitQuantity ) { $unitPrice = $unit->sale_price * $procurementProduct->quantity; } /** * We'll return the total purchase * price to update the procurement total fees. */ return [ 'total_purchase_price' => $procurementProduct->total_purchase_price, 'tax_value' => $procurementProduct->tax_value, 'total_price' => $unitPrice, ]; } ); $procurement->cost = $purchases->sum( 'total_purchase_price' ); $procurement->tax_value = $purchases->sum( 'tax_value' ); $procurement->value = $purchases->sum( 'total_price' ); $procurement->total_items = count( $purchases ); $procurement->save(); } ); return [ 'status' => 'success', 'message' => __( 'The procurement has been refreshed.' ), 'data' => compact( 'procurement' ), ]; } /** * delete procurement * products * * @param Procurement * @return array */ public function deleteProducts( Procurement $procurement ) { $procurement->products->each( function ( $product ) { $product->delete(); } ); return [ 'status' => 'success', 'message' => __( 'The procurement products has been deleted.' ), ]; } /** * helps to determine if a procurement * includes a specific product using their id. * The ID of the product should be the one of the products of the procurements * * @param int procurement id * @param int product id */ public function hasProduct( int $procurement_id, int $product_id ) { $procurement = $this->get( $procurement_id ); return $procurement->products->filter( function ( $product ) use ( $product_id ) { return (int) $product->id === (int) $product_id; } )->count() > 0; } /** * @deprecated */ public function updateProcurementProduct( $product_id, $fields ) { $procurementProduct = $this->getProcurementProduct( $product_id ); $item = $this->productService->get( $procurementProduct->product_id ); $storedUnitReference = []; $itemsToSave = []; /** * the idea here it to update the procurement * quantity, unit_id and purchase price, since that information * is used on __computeProcurementProductValues */ foreach ( $fields as $field => $value ) { $procurementProduct->$field = $value; } /** * @var array $itemsToSave * @var array errors */ extract( $this->__computeProcurementProductValues( compact( 'item', 'procurementProduct', 'storeUnitReference', 'itemsToSave', 'errors' ) ) ); /** * typically since the items to save should be * only a single entry, we'll harcode it to be "0" */ foreach ( $itemsToSave[0] as $field => $value ) { $procurementProduct->$field = $value; } $procurementProduct->author_id = Auth::id(); $procurementProduct->save(); return [ 'status' => 'success', 'message' => __( 'The procurement product has been updated.' ), 'data' => [ 'product' => $procurementProduct, ], ]; } public function getProcurementProduct( $product_id ) { $product = ProcurementProduct::find( $product_id ); if ( ! $product instanceof ProcurementProduct ) { throw new Exception( __( 'Unable to find the procurement product using the provided id.' ) ); } return $product; } /** * Delete a procurement product * * @param int procurement product id * @return array response */ public function deleteProduct( ProcurementProduct $procurementProduct, Procurement $procurement ) { /** * this could be useful to prevent deletion for * product which might be in use by another resource */ event( new ProcurementBeforeDeleteProductEvent( $procurementProduct ) ); /** * we'll reduce the stock only if the * procurement has been stocked. */ if ( $procurement->delivery_status === 'stocked' ) { /** * if the product wasn't convered into a different unit * then we'll directly perform a stock adjustment on that product. */ if ( ! empty( $procurementProduct->convert_unit_id ) ) { $from = Unit::find( $procurementProduct->unit_id ); $to = Unit::find( $procurementProduct->convert_unit_id ); $convertedQuantityToRemove = $this->unitService->getConvertedQuantity( from: $from, to: $to, quantity: $procurementProduct->quantity ); $purchasePrice = $this->unitService->getPurchasePriceFromUnit( purchasePrice: $procurementProduct->purchase_price, from: $from, to: $to ); $this->productService->stockAdjustment( ProductHistory::ACTION_DELETED, [ 'total_price' => ns()->currency->define( $purchasePrice )->multipliedBy( $convertedQuantityToRemove )->toFloat(), 'unit_price' => $purchasePrice, 'unit_id' => $procurementProduct->convert_unit_id, 'product_id' => $procurementProduct->product_id, 'quantity' => $convertedQuantityToRemove, 'procurementProduct' => $procurementProduct, ] ); } else { /** * Record the deletion on the product * history */ $this->productService->stockAdjustment( ProductHistory::ACTION_DELETED, [ 'total_price' => $procurementProduct->total_purchase_price, 'unit_price' => $procurementProduct->purchase_price, 'unit_id' => $procurementProduct->unit_id, 'product_id' => $procurementProduct->product_id, 'quantity' => $procurementProduct->quantity, 'procurementProduct' => $procurementProduct, ] ); } } $procurementProduct->delete(); /** * the product has been deleted, so we couldn't pass * the Model Object anymore */ event( new ProcurementAfterDeleteProductEvent( $procurementProduct->id, $procurement ) ); return [ 'status' => 'sucecss', 'message' => sprintf( __( 'The product %s has been deleted from the procurement %s' ), $procurementProduct->name, $procurement->name, ), ]; } public function getProcurementProducts( $procurement_id ) { return ProcurementProduct::getByProcurement( $procurement_id ) ->get(); } /** * Update a procurement products * using the provided product collection * * @param int procurement id * @param array array * @return array status * * @deprecated */ public function bulkUpdateProducts( $procurement_id, $products ) { $productsId = $this->getProcurementProducts( $procurement_id ) ->pluck( 'id' ); $result = collect( $products ) ->map( function ( $product ) use ( $productsId ) { if ( ! in_array( $product[ 'id' ], $productsId ) ) { throw new Exception( sprintf( __( 'The product with the following ID "%s" is not initially included on the procurement' ), $product[ 'id' ] ) ); } return $product; } ) ->map( function ( $product ) { return $this->updateProcurementProduct( $product[ 'id' ], $product ); } ); return [ 'status' => 'success', 'message' => __( 'The procurement products has been updated.' ), 'data' => compact( 'result' ), ]; } /** * Get the procurements product * * @param int procurement id */ public function getProducts( $procurement_id ): EloquentCollection { $procurement = $this->get( $procurement_id ); return $procurement->products; } public function setDeliveryStatus( Procurement $procurement, string $status ) { Procurement::withoutEvents( function () use ( $procurement, $status ) { $procurement->delivery_status = $status; $procurement->save(); } ); } /** * When a procurement is being made * this will actually save the history and update * the product stock */ public function handleProcurement( Procurement $procurement ): void { DB::transaction( fn() => $this->persistHandledProcurement( $procurement ), attempts: 3 ); } private function persistHandledProcurement( Procurement $procurement ): void { event( new ProcurementBeforeHandledEvent( $procurement ) ); if ( $procurement->delivery_status === Procurement::DELIVERED ) { $procurement->products->map( function ( ProcurementProduct $product ) { /** * We'll keep an history of what has just happened. * in order to monitor how the stock evolve. */ $this->productService->saveHistory( ProductHistory::ACTION_STOCKED, [ 'procurement_id' => $product->procurement_id, 'product_id' => $product->product_id, 'procurement_product_id' => $product->id, 'operation_type' => ProductHistory::ACTION_STOCKED, 'quantity' => $product->quantity, 'unit_price' => $product->purchase_price, 'total_price' => $product->total_purchase_price, 'unit_id' => $product->unit_id, ] ); $currentQuantity = $this->productService->getQuantity( $product->product_id, $product->unit_id, $product->id ); $newQuantity = $this->currency ->define( $currentQuantity ) ->additionateBy( $product->quantity ) ->get(); $this->productService->setQuantity( $product->product_id, $product->unit_id, $newQuantity, $product->id ); /** * will generate a unique barcode for the procured product */ $this->generateBarcode( $product ); /** * We'll now check if the product is about to be * converted in another unit */ if ( ! empty( $product->convert_unit_id ) ) { $this->productService->convertUnitQuantities( product: $product->product, quantity: $product->quantity, from: $product->unit, procurementProduct: $product, to: Unit::find( $product->convert_unit_id ) ); } } ); $this->setDeliveryStatus( $procurement, Procurement::STOCKED ); } event( new ProcurementAfterHandledEvent( $procurement ) ); } public function generateBarcode( ProcurementProduct $procurementProduct ) { $this->barcodeService->generateBarcode( $procurementProduct->barcode, BarcodeService::TYPE_CODE128 ); } /** * Make sure to procure procurement that * are awaiting auto-submittion * * @return void */ public function stockAwaitingProcurements() { $startOfDay = $this->dateService->copy(); $procurements = Procurement::where( 'delivery_time', '<=', $startOfDay ) ->pending() ->autoApproval() ->get(); $procurements->each( function ( Procurement $procurement ) { $this->setDeliveryStatus( $procurement, Procurement::DELIVERED ); $this->handleProcurement( $procurement ); } ); if ( $procurements->count() ) { ns()->notification->create( title: __( 'Procurement Automatically Stocked' ), identifier: 'ns-warn-auto-procurement', url: url( '/dashboard/procurements' ), description: sprintf( __( '%s procurement(s) has recently been automatically procured.' ), $procurements->count() ), )->dispatchForGroup( [ Role::namespace( 'admin' ), Role::namespace( 'nexopos.store.administrator' ), ] ); } } public function getDeliveryLabel( $label ) { switch ( $label ) { case Procurement::DELIVERED: return __( 'Delivered' ); case Procurement::DRAFT: return __( 'Draft' ); case Procurement::PENDING: return __( 'Pending' ); case Procurement::STOCKED: return __( 'Stocked' ); default: return $label; } } public function getPaymentLabel( $label ) { switch ( $label ) { case Procurement::PAYMENT_PAID: return __( 'Paid' ); case Procurement::PAYMENT_UNPAID: return __( 'Unpaid' ); default: return $label; } } public function searchQuery() { return Product::query() ->whereIn( 'type', Hook::filter( 'ns-procurement-searchable-product-type', [ Product::TYPE_DEMATERIALIZED, Product::TYPE_MATERIALIZED, ] ) ) ->notGrouped() ->withStockEnabled() ->with( 'unit_quantities.unit' ); } public function searchProduct( $argument, $limit = 10 ) { return $this->searchQuery() ->limit( $limit ) ->where( function ( $query ) use ( $argument ) { $query->orWhere( 'name', 'LIKE', "%{$argument}%" ) ->orWhere( 'sku', 'LIKE', "%{$argument}%" ) ->orWhere( 'barcode', 'LIKE', "%{$argument}%" ); } ) ->get() ->map( function ( $product ) { return $this->populateLoadedProduct( $product ); } ); } public function populateLoadedProduct( $product ) { $units = json_decode( $product->purchase_unit_ids ); if ( $units ) { $product->purchase_units = collect(); collect( $units )->each( function ( $unitID ) use ( &$product ) { $product->purchase_units->push( Unit::find( $unitID ) ); } ); } /** * We'll pull the last purchase * price for the item retrieved */ $product->unit_quantities->each( function ( $unitQuantity ) use ( $product ) { $unitQuantity->load( 'unit' ); /** * just in case it's not a valid instance * we'll provide a default value "0" */ $unitQuantity->last_purchase_price = $this->productService->getLastPurchasePrice( product: $product, unit: $unitQuantity->unit ); } ); return $product; } public function searchProcurementProduct( $argument ) { $procurementProduct = ProcurementProduct::where( 'barcode', $argument ) ->with( [ 'unit', 'procurement' ] ) ->first(); if ( $procurementProduct instanceof ProcurementProduct ) { $procurementProduct->unit_quantity = $this->productService->getUnitQuantity( $procurementProduct->product_id, $procurementProduct->unit_id ); } return $procurementProduct; } public function preload( string $hash ) { if ( Cache::has( 'procurements-' . $hash ) ) { $data = Cache::get( 'procurements-' . $hash ); return [ 'items' => collect( $data[ 'items' ] )->map( function ( $item ) use ( $data ) { $query = $this->searchQuery() ->where( 'id', $item[ 'product_id' ] ) ->whereHas( 'unit_quantities', function ( $query ) use ( $item ) { $query->where( 'unit_id', $item[ 'unit_id' ] ); } ); $product = $query->first(); if ( $product instanceof Product ) { /** * This will be helpful to set the desired unit * and quantity provided on the preload configuration. */ $product->procurement = new stdClass; $product->procurement->unit_id = $item[ 'unit_id' ]; $product->procurement->quantity = ns()->currency ->define( $item[ 'quantity' ] ) ->multipliedBy( $data[ 'multiplier' ] )->toFloat(); return $this->populateLoadedProduct( $product ); } return false; } )->filter(), ]; } throw new NotFoundException( __( 'Unable to preload products. The hash might have expired or is invalid.' ) ); } public function storePreload( string $hash, Collection|array $items, $expiration = 86400, $multiplier = 1 ) { if ( ! empty( $items ) ) { $data = []; $data[ 'multiplier' ] = $multiplier; $data[ 'items' ] = $items; Cache::put( 'procurements-' . $hash, $data, $expiration ); return [ 'status' => 'success', 'message' => __( 'The procurement has been saved for later use.' ), ]; } throw new Exception( __( 'Unable to save the procurement for later use.' ) ); } public function getLowStockSuggestions() { return ProductUnitQuantity::query() ->stockAlertEnabled() ->whereRaw( 'low_quantity > quantity' ) ->with( [ 'product' => function ( $query ) { $query->select( 'id', 'name', 'sku', 'barcode', 'tax_group_id', 'tax_type' ) ->notGrouped() ->withStockEnabled(); }, 'unit', ] ) ->whereHas( 'product', function ( $query ) { $query->notGrouped() ->withStockEnabled(); } ) ->limit( 50 ) ->get() ->map( function ( $unitQuantity ) { $product = Product::find( $unitQuantity->product_id )->load( 'unit_quantities.unit' ); /** * If we've made a procurement of this product recently, * we'll pull the recent quantity procured for that product */ $procurementProduct = ProcurementProduct::where( 'product_id', $unitQuantity->product_id ) ->where( 'unit_id', $unitQuantity->unit_id ) ->whereHas( 'procurement', function ( $query ) { $query->where( 'delivery_status', Procurement::STOCKED ); } ) ->orderBy( 'created_at', 'DESC' ) ->first(); if ( $procurementProduct instanceof ProcurementProduct ) { $product->recent_procured_quantity = $procurementProduct->quantity; $product->recent_procured_unit_id = $procurementProduct->unit_id; $product->recent_procured_convert_unit_id = $procurementProduct->convert_unit_id; $product->recent_procured_purchase_price = $procurementProduct->purchase_price; if ( ! empty( $procurementProduct->convert_unit_id ) ) { $convertUnit = Unit::find( $procurementProduct->convert_unit_id ); $product->recent_procured_convert_unit_label = $convertUnit instanceof Unit ? $convertUnit->label : null; } else { $product->recent_procured_convert_unit_label = null; } } else { $product->recent_procured_quantity = $product->low_stock_quantity; $product->recent_procured_unit_id = $unitQuantity->unit_id; $product->recent_procured_convert_unit_id = null; $product->recent_procured_convert_unit_label = null; } return $product; } ); } }