productUtils = $productUtils; } /** * Splits variations by choices label. Removes label from a product, creates n-1 new copies of the product for each n label values * and splits the variations to them. If there's no label left on new products after operation, it removes last variation from products and * copies data of variation to product. * * @param $idProduct int * @param $idLabel int * @param false $nameAppend */ public function splitByLabel($idProduct, $idLabel, $nameAppend = false) { $productsCreated = []; sqlGetConnection()->transactional( function () use ($idProduct, $idLabel, $nameAppend, &$productsCreated) { $productLabelsVariations = sqlFetchAll( sqlQueryBuilder()->select("GROUP_CONCAT(pv.id SEPARATOR ',') variations, pvc.id_value, pvcv.value") ->from('products_variations', 'pv') ->innerJoin('pv', 'products_variations_combination', 'pvc', 'pvc.id_variation = pv.id') ->innerJoin('pvc', 'products_variations_choices_values', 'pvcv', 'pvcv.id = pvc.id_value') ->where( Operator::equals( [ 'pv.id_product' => $idProduct, 'pvc.id_label' => $idLabel, ] ) ) ->groupBy('pvc.id_value') ->execute(), 'id_value' ); $this->removeLabelFromProduct($idProduct, $idLabel); $productLabels = $this->getChoicesLabels($idProduct); $originalProductLabel = array_key_last($productLabelsVariations); foreach ($productLabelsVariations as $idLabel => $productLabelVariations) { $variations = explode(',', $productLabelVariations['variations']); $newIdProduct = $idProduct; if ($originalProductLabel != $idLabel) { $newIdProduct = $this->productUtils->duplicateProduct($idProduct); $productsCreated[] = $newIdProduct; $this->productUtils->copyProductData($idProduct, $newIdProduct, ['_all_' => true, 'pricelist' => 'products', 'photos' => 'products', 'variations' => false]); foreach ($productLabels ?? [] as $label) { sqlQueryBuilder()->insert('products_variations_choices_categorization') ->directValues( [ 'id_product' => $newIdProduct, 'id_label' => $label['id_label'], 'list_order' => $label['list_order'], ] ) ->execute(); } $this->updateVariationProductId($variations, $newIdProduct); } if ($productLabels) { $this->fixProductPrice($newIdProduct); } if ($nameAppend) { $this->appendProductTitle($newIdProduct, $productLabelVariations['value']); } } \Variations::recalcInStore(); if (!$productLabels) { foreach ($productLabelsVariations as $productLabelVariations) { $variations = explode(',', $productLabelVariations['variations']); if (count($variations) == 1) { $this->updateProductFromVariation($variations[0]); } else { throw new \Exception('A product without label has more than one variation.'); } } } } ); return $productsCreated; } private function getChoicesLabels($idProduct) { return sqlQueryBuilder()->select('id_label, list_order') ->from('products_variations_choices_categorization') ->where( Operator::equals( [ 'id_product' => $idProduct, ] ) ) ->execute()->fetchAll(); } /** * Moves data to a product from its last variation and removes the variation. */ private function updateProductFromVariation($idVariation) { $fields = [ 'id_product', 'code', 'price', 'ean', 'in_store', 'delivery_time', 'figure', 'updated', 'date_added', 'width', 'height', 'depth', ]; if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY)) { $fields[] = 'price_buy'; } if (findModule(\Modules::PRODUCTS, \Modules::SUB_NOTE)) { $fields[] = 'note'; } if (findModule(\Modules::BONUS_PROGRAM)) { $fields[] = 'bonus_points'; } if (findModule('products', 'weight')) { $fields[] = 'weight'; } if (findModule(\Modules::MISSING_PRODUCTS)) { $fields[] = 'in_store_min'; } $variationFields = sqlQueryBuilder() ->select(implode(',', $fields)) ->from('products_variations') ->where(Operator::equals(['id' => $idVariation])) ->execute()->fetch(); $idProduct = $variationFields['id_product']; unset($variationFields['id_product']); $this->cleanPriceListVariations($idProduct); $this->removeVariationForeignKeys($idVariation); sqlQueryBuilder()->delete('products_variations') ->where(Operator::equals(['id' => $idVariation])) ->execute(); sqlQueryBuilder()->update('products') ->directValues( array_filter( $variationFields, function ($field) { return !empty($field); } ) ) ->where(Operator::equals(['id' => $idProduct])) ->execute(); } /** * Updates product ID of variations to given product ID in multiple places. * * @param $variations array * @param $newIdProduct int */ private function updateVariationProductId($variations, $newIdProduct) { $update = function ($table, $idProductField = 'id_product', $idVariationField = 'id_variation') use ($variations, $newIdProduct) { sqlQueryBuilder()->update($table) ->directValues([$idProductField => $newIdProduct]) ->where(Operator::inStringArray($variations, $idVariationField)) ->execute(); }; $update('products_variations', 'id_product', 'id'); $this->productUtils->updateTablesWithCallback($update); } private function cleanPriceListVariations($idProduct): void { if (findModule(\Modules::PRICELISTS)) { sqlQueryBuilder() ->delete('pricelists_products') ->andWhere(Operator::equals(['id_product' => $idProduct])) ->andWhere(Operator::isNull('id_variation')) ->execute(); } } /** * Sets null to all foreign keys of a variation. */ private function removeVariationForeignKeys($idVariation) { $update = function ($table, $idProductField = null, $idVariationField = 'id_variation') use ($idVariation) { sqlQueryBuilder()->update($table) ->directValues([$idVariationField => null]) ->where(Operator::equals([$idVariationField => $idVariation])) ->execute(); }; $this->productUtils->updateTablesWithCallback($update); } /** * Sets the most frequent price of variations to a product and sets null to the variations with that price. * If the variations already have null price before operation, then nothing happens. * * @param $idProduct int */ public function fixProductPrice($idProduct) { $hasNullPriceVariation = sqlQueryBuilder()->select('id') ->from('products_variations', 'pv') ->where(Operator::equals(['id_product' => $idProduct])) ->andWhere(Operator::equalsNullable(['price' => null])) ->execute()->fetch(); if ($hasNullPriceVariation) { return; } $topPrice = sqlQueryBuilder()->select("price, GROUP_CONCAT(id SEPARATOR ',') variations") ->from('products_variations', 'pv') ->where(Operator::equals(['id_product' => $idProduct])) ->groupBy('price') ->orderBy('price') ->setMaxResults(1) ->execute()->fetch(); sqlQueryBuilder()->update('products') ->directValues(['price' => $topPrice['price']]) ->where(Operator::equals(['id' => $idProduct])) ->execute(); sqlQueryBuilder()->update('products_variations') ->set('price', 'null') ->where(Operator::inStringArray(explode(',', $topPrice['variations']), 'id')) ->execute(); } /** * Appends value to the title of product. * * @param $idProduct int * @param $value string */ private function appendProductTitle($idProduct, $value) { sqlQueryBuilder()->update('products') ->set('title', "CONCAT(title, ' - ', :append)") ->where(Operator::equals(['id' => $idProduct])) ->setParameter('append', $value) ->execute(); } /** * Removes label from a product and its variations. * * @param $idProduct int * @param $idLabel int */ private function removeLabelFromProduct($idProduct, $idLabel) { $variations = sqlFetchAll( sqlQueryBuilder()->select('id') ->from('products_variations') ->where(Operator::equals(['id_product' => $idProduct])) ->execute(), ['id' => 'id'] ); sqlQueryBuilder()->delete('products_variations_combination') ->where(Operator::equals(['id_label' => $idLabel])) ->andWhere(Operator::inStringArray(array_values($variations), 'id_variation')) ->execute(); sqlQueryBuilder()->delete('products_variations_choices_categorization') ->where( Operator::equals( [ 'id_product' => $idProduct, 'id_label' => $idLabel, ] ) ) ->execute(); } }