diff --git a/assets/styles/admin/libs/tooltip/tooltip-custom.less b/assets/styles/admin/libs/tooltip/tooltip-custom.less index 69f032d105..f963507831 100644 --- a/assets/styles/admin/libs/tooltip/tooltip-custom.less +++ b/assets/styles/admin/libs/tooltip/tooltip-custom.less @@ -4,4 +4,7 @@ .tooltip-inner { min-width: 120px; -} \ No newline at end of file + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; +} diff --git a/src/Component/Plugin/PluginCrudExtensionRegistry.php b/src/Component/Plugin/PluginCrudExtensionRegistry.php index c7a67cf4cb..f00391a0f9 100644 --- a/src/Component/Plugin/PluginCrudExtensionRegistry.php +++ b/src/Component/Plugin/PluginCrudExtensionRegistry.php @@ -14,6 +14,7 @@ class PluginCrudExtensionRegistry protected const KNOWN_TYPES = [ 'product', 'category', + 'stockSettings', ]; /** diff --git a/src/Component/Setting/Setting.php b/src/Component/Setting/Setting.php index c63e0ac42d..044c9f2a54 100644 --- a/src/Component/Setting/Setting.php +++ b/src/Component/Setting/Setting.php @@ -10,23 +10,24 @@ class Setting { - public const PERSONAL_DATA_DISPLAY_SITE_CONTENT = 'personalDataDisplaySiteContent'; - public const PERSONAL_DATA_EXPORT_SITE_CONTENT = 'personalDataExportSiteContent'; - public const DEFAULT_PRICING_GROUP = 'defaultPricingGroupId'; - public const TERMS_AND_CONDITIONS_ARTICLE_ID = 'termsAndConditionsArticleId'; - public const PRIVACY_POLICY_ARTICLE_ID = 'privacyPolicyArticleId'; - public const USER_CONSENT_POLICY_ARTICLE_ID = 'userConsentPolicyArticleId'; - public const DOMAIN_DATA_CREATED = 'domainDataCreated'; - public const FEED_HASH = 'feedHash'; - public const DEFAULT_UNIT = 'defaultUnitId'; - public const BASE_URL = 'baseUrl'; - public const FEED_NAME_TO_CONTINUE = 'feedNameToContinue'; - public const FEED_DOMAIN_ID_TO_CONTINUE = 'feedDomainIdToContinue'; - public const FEED_ITEM_ID_TO_CONTINUE = 'feedItemIdToContinue'; - public const TRANSFER_DAYS_BETWEEN_STOCKS = 'transferDaysBetweenStocks'; - public const IMAGE_STRUCTURE_MIGRATED_FOR_PROXY = 'imageStructureMigratedForProxy'; - public const CUSTOMER_USER_DEFAULT_GROUP_ROLE_ID = 'customerUserDefaultGroupRoleId'; - public const FILE_STRUCTURE_MIGRATED_FOR_RELATIONS = 'fileStructureMigratedForRelations'; + public const string PERSONAL_DATA_DISPLAY_SITE_CONTENT = 'personalDataDisplaySiteContent'; + public const string PERSONAL_DATA_EXPORT_SITE_CONTENT = 'personalDataExportSiteContent'; + public const string DEFAULT_PRICING_GROUP = 'defaultPricingGroupId'; + public const string TERMS_AND_CONDITIONS_ARTICLE_ID = 'termsAndConditionsArticleId'; + public const string PRIVACY_POLICY_ARTICLE_ID = 'privacyPolicyArticleId'; + public const string USER_CONSENT_POLICY_ARTICLE_ID = 'userConsentPolicyArticleId'; + public const string DOMAIN_DATA_CREATED = 'domainDataCreated'; + public const string FEED_HASH = 'feedHash'; + public const string DEFAULT_UNIT = 'defaultUnitId'; + public const string BASE_URL = 'baseUrl'; + public const string FEED_NAME_TO_CONTINUE = 'feedNameToContinue'; + public const string FEED_DOMAIN_ID_TO_CONTINUE = 'feedDomainIdToContinue'; + public const string FEED_ITEM_ID_TO_CONTINUE = 'feedItemIdToContinue'; + public const string TRANSFER_DAYS_BETWEEN_STOCKS = 'transferDaysBetweenStocks'; + public const string FEED_DELIVERY_DAYS_FOR_OUT_OF_STOCK_PRODUCTS = 'feedDeliveryDaysForOutOfStockProducts'; + public const string IMAGE_STRUCTURE_MIGRATED_FOR_PROXY = 'imageStructureMigratedForProxy'; + public const string CUSTOMER_USER_DEFAULT_GROUP_ROLE_ID = 'customerUserDefaultGroupRoleId'; + public const string FILE_STRUCTURE_MIGRATED_FOR_RELATIONS = 'fileStructureMigratedForRelations'; /** * @var \Shopsys\FrameworkBundle\Component\Setting\SettingValue[][] @@ -166,6 +167,14 @@ protected function loadDomainValues($domainId) } } + /** + * @param string $name + */ + public function deleteByName(string $name): void + { + $this->settingValueRepository->deleteByName($name); + } + public function clearCache() { $this->allValuesLoaded = false; diff --git a/src/Component/Setting/SettingValueRepository.php b/src/Component/Setting/SettingValueRepository.php index 96c28d9398..b80c70c193 100644 --- a/src/Component/Setting/SettingValueRepository.php +++ b/src/Component/Setting/SettingValueRepository.php @@ -75,4 +75,17 @@ public function copyAllMultidomainSettings($fromDomainId, $toDomainId) ], ); } + + /** + * @param string $name + */ + public function deleteByName(string $name): void + { + $this->getSettingValueRepository()->createQueryBuilder('sv') + ->delete(SettingValue::class, 'sv') + ->where('sv.name = :name') + ->setParameter('name', $name) + ->getQuery() + ->execute(); + } } diff --git a/src/Controller/Admin/StockController.php b/src/Controller/Admin/StockController.php index 9ce012e86e..a9d8cb2554 100644 --- a/src/Controller/Admin/StockController.php +++ b/src/Controller/Admin/StockController.php @@ -106,7 +106,7 @@ public function saveSettingsAction(Request $request): RedirectResponse $this ->addSuccessFlashTwig( t( - 'Warehouse setting %domainName% saved.', + 'Setting for domain "%domainName%" was saved.', [ '%domainName%' => $this->adminDomainTabsFacade->getSelectedDomainConfig()->getName(), ], diff --git a/src/Form/Admin/Payment/PaymentFormType.php b/src/Form/Admin/Payment/PaymentFormType.php index e239c9441a..c5783bcdc4 100644 --- a/src/Form/Admin/Payment/PaymentFormType.php +++ b/src/Form/Admin/Payment/PaymentFormType.php @@ -13,8 +13,8 @@ use Shopsys\FrameworkBundle\Form\GroupType; use Shopsys\FrameworkBundle\Form\ImageUploadType; use Shopsys\FrameworkBundle\Form\Locale\LocalizedType; +use Shopsys\FrameworkBundle\Form\MessageType; use Shopsys\FrameworkBundle\Form\PriceAndVatTableByDomainsType; -use Shopsys\FrameworkBundle\Form\WarningMessageType; use Shopsys\FrameworkBundle\Model\GoPay\PaymentMethod\GoPayPaymentMethod; use Shopsys\FrameworkBundle\Model\GoPay\PaymentMethod\GoPayPaymentMethodFacade; use Shopsys\FrameworkBundle\Model\Payment\Payment; @@ -294,7 +294,7 @@ public function addHiddenByGoPayWarning(PaymentData $paymentData, FormBuilderInt $domainNames[] = $this->domain->getDomainConfigById($domainId)->getName(); } - $builder->add('hiddenByGoPay', WarningMessageType::class, [ + $builder->add('hiddenByGoPay', MessageType::class, [ 'data' => t('This payment method is hidden by GoPay on domains: %domains%', [ '%domains%' => implode(', ', $domainNames), ]), diff --git a/src/Form/Admin/Product/ProductFormType.php b/src/Form/Admin/Product/ProductFormType.php index c13bbaf29b..42fef68d3d 100644 --- a/src/Form/Admin/Product/ProductFormType.php +++ b/src/Form/Admin/Product/ProductFormType.php @@ -23,13 +23,13 @@ use Shopsys\FrameworkBundle\Form\ImageUploadType; use Shopsys\FrameworkBundle\Form\Locale\LocalizedType; use Shopsys\FrameworkBundle\Form\LocalizedFullWidthType; +use Shopsys\FrameworkBundle\Form\MessageType; use Shopsys\FrameworkBundle\Form\MultiLocaleFileUploadType; use Shopsys\FrameworkBundle\Form\ProductParameterValueType; use Shopsys\FrameworkBundle\Form\ProductsType; use Shopsys\FrameworkBundle\Form\Transformers\ProductParameterValueToProductParameterValuesLocalizedTransformer; use Shopsys\FrameworkBundle\Form\Transformers\RemoveDuplicatesFromArrayTransformer; use Shopsys\FrameworkBundle\Form\UrlListType; -use Shopsys\FrameworkBundle\Form\WarningMessageType; use Shopsys\FrameworkBundle\Model\Category\CategoryFacade; use Shopsys\FrameworkBundle\Model\Product\Brand\BrandFacade; use Shopsys\FrameworkBundle\Model\Product\Flag\FlagFacade; @@ -107,18 +107,39 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]; } - $builder->add('name', LocalizedFullWidthType::class, [ - 'required' => false, - 'entry_options' => [ - 'constraints' => [ - new Constraints\Length( - ['max' => 255, 'maxMessage' => 'Product name cannot be longer than {{ limit }} characters'], - ), + $builder + ->add('namePrefix', LocalizedFullWidthType::class, [ + 'required' => false, + 'entry_options' => [ + 'constraints' => [ + new Constraints\Length(['max' => 255, 'maxMessage' => 'Product prefix name cannot be longer than {{ limit }} characters']), + ], ], - ], - 'label' => t('Name'), - 'render_form_row' => false, - ]); + 'label' => t('Name prefix'), + 'render_form_row' => false, + ]) + ->add('name', LocalizedFullWidthType::class, [ + 'required' => false, + 'entry_options' => [ + 'constraints' => [ + new Constraints\Length( + ['max' => 255, 'maxMessage' => 'Product name cannot be longer than {{ limit }} characters'], + ), + ], + ], + 'label' => t('Name'), + 'render_form_row' => false, + ]) + ->add('nameSuffix', LocalizedFullWidthType::class, [ + 'required' => false, + 'entry_options' => [ + 'constraints' => [ + new Constraints\Length(['max' => 255, 'maxMessage' => 'Product suffix name cannot be longer than {{ limit }} characters']), + ], + ], + 'label' => t('Name suffix'), + 'render_form_row' => false, + ]); if ($this->isProductVariant($product) || $this->isProductMainVariant($product)) { $builder->add($this->createVariantGroup($builder, $product)); @@ -127,7 +148,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder->add($this->createBasicInformationGroup($builder, $product, $disabledItemInMainVariantAttr)); $builder->add($this->createDisplayAvailabilityGroup($builder, $product)); $builder->add($this->createPricesGroup($builder, $product)); - $builder->add($this->createStocksGroup($builder)); + $builder->add($this->createStocksGroup($builder, $product)); $builder->add($this->createDescriptionsGroup($builder, $product)); $builder->add($this->createShortDescriptionsGroup($builder, $product)); $builder->add($this->createShortDescriptionsUspGroup($builder)); @@ -434,7 +455,7 @@ private function createDisplayAvailabilityGroup( && $product->getCalculatedSellingDenied() ) { $builderDisplayAvailabilityGroup - ->add('productCalculatedSellingDeniedInfo', WarningMessageType::class, [ + ->add('productCalculatedSellingDeniedInfo', MessageType::class, [ 'data' => t('Product is excluded from the sale'), ]); } @@ -505,19 +526,27 @@ private function createDisplayAvailabilityGroup( /** * @param \Symfony\Component\Form\FormBuilderInterface $builder + * @param \Shopsys\FrameworkBundle\Model\Product\Product|null $product * @return \Symfony\Component\Form\FormBuilderInterface */ - private function createStocksGroup(FormBuilderInterface $builder) + private function createStocksGroup(FormBuilderInterface $builder, ?Product $product): FormBuilderInterface { $stockGroupBuilder = $builder->create('stocksGroup', GroupType::class, [ 'label' => t('Warehouses'), ]); - $stockGroupBuilder->add('productStockData', CollectionType::class, [ - 'required' => false, - 'entry_type' => ProductStockFormType::class, - 'render_form_row' => false, - ]); + if ($this->isProductMainVariant($product)) { + $stockGroupBuilder + ->add('productStockData', DisplayOnlyType::class, [ + 'data' => t('The stock quantities are set for the product variants separately.'), + ]); + } else { + $stockGroupBuilder->add('productStockData', CollectionType::class, [ + 'required' => false, + 'entry_type' => ProductStockFormType::class, + 'render_form_row' => false, + ]); + } return $stockGroupBuilder; } diff --git a/src/Form/Admin/Stock/ProductStockFormType.php b/src/Form/Admin/Stock/ProductStockFormType.php index e3702cd04b..71b936cbc1 100644 --- a/src/Form/Admin/Stock/ProductStockFormType.php +++ b/src/Form/Admin/Stock/ProductStockFormType.php @@ -24,8 +24,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'placeholder' => '0', ], 'constraints' => [ - new Constraints\GreaterThanOrEqual(['value' => 0]), - new Constraints\Regex(['pattern' => '/^\d+$/']), + new Constraints\Regex([ + 'pattern' => '/^-?\d+$/', + 'message' => 'Quantity must be an integer', + ]), ], ]); } diff --git a/src/Form/Admin/Stock/StockSettingsFormType.php b/src/Form/Admin/Stock/StockSettingsFormType.php index 01b9d0cc4f..467d5df1e4 100644 --- a/src/Form/Admin/Stock/StockSettingsFormType.php +++ b/src/Form/Admin/Stock/StockSettingsFormType.php @@ -4,22 +4,41 @@ namespace Shopsys\FrameworkBundle\Form\Admin\Stock; +use Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade; +use Shopsys\FrameworkBundle\Form\GroupType; +use Shopsys\FrameworkBundle\Form\MessageType; use Shopsys\FrameworkBundle\Model\Stock\StockSettingsData; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints; +use Twig\Environment; class StockSettingsFormType extends AbstractType { + /** + * @param \Twig\Environment $environment + * @param \Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade $pluginCrudExtensionFacade + */ + public function __construct( + protected readonly Environment $environment, + protected readonly PluginCrudExtensionFacade $pluginCrudExtensionFacade, + ) { + } + /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options): void { - $builder + $builderStockSettingGroup = $builder->create('stockSettings', GroupType::class, [ + 'label' => t('Warehouse settings'), + ]); + + $builderStockSettingGroup ->add('transfer', TextType::class, [ 'label' => t('Days for transfer between warehouses'), 'constraints' => [ @@ -27,8 +46,33 @@ public function buildForm(FormBuilderInterface $builder, array $options): void new Constraints\Regex(['pattern' => '/^\d+$/']), new Constraints\GreaterThanOrEqual(['value' => 0]), ], + ]); + + $builderFeedSettingGroup = $builder->create('feedSettings', GroupType::class, [ + 'label' => t('XML feeds settings'), + ]); + + $builderFeedSettingGroup + ->add('feedDeliveryDaysForOutOfStockProducts', IntegerType::class, [ + 'label' => t('Number of delivery days for out of stock products in XML feeds'), + 'required' => true, + 'constraints' => [ + new Constraints\NotNull([ + 'message' => 'Please enter the number of delivery days.', + ]), + ], ]) + ->add('feedDeliveryDaysForOutOfStockProductsInfo', MessageType::class, [ + 'message_level' => MessageType::MESSAGE_LEVEL_INFO, + 'data' => $this->environment->render('@ShopsysFramework/Admin/Content/Feed/feedDeliveryDaysForOutOfStockProductsInfo.html.twig'), + ]); + + $builder + ->add($builderStockSettingGroup) + ->add($builderFeedSettingGroup) ->add('save', SubmitType::class); + + $this->pluginCrudExtensionFacade->extendForm($builder, 'stockSettings', 'pluginData'); } /** @@ -39,6 +83,7 @@ public function configureOptions(OptionsResolver $resolver): void $resolver ->setDefaults([ 'data_class' => StockSettingsData::class, + 'attr' => ['novalidate' => 'novalidate'], ]); } } diff --git a/src/Form/MessageType.php b/src/Form/MessageType.php new file mode 100644 index 0000000000..8e3741ca82 --- /dev/null +++ b/src/Form/MessageType.php @@ -0,0 +1,44 @@ +setDefined('message_level') + ->setAllowedValues('message_level', [self::MESSAGE_LEVEL_WARNING, self::MESSAGE_LEVEL_INFO]) + ->setDefaults([ + 'mapped' => false, + 'required' => false, + 'message_level' => self::MESSAGE_LEVEL_WARNING, + ]); + } + + /** + * {@inheritdoc} + */ + #[Override] + public function buildView(FormView $view, FormInterface $form, array $options): void + { + parent::buildView($view, $form, $options); + + $view->vars['message_level'] = $options['message_level']; + } +} diff --git a/src/Form/WarningMessageType.php b/src/Form/WarningMessageType.php deleted file mode 100644 index a916f0a40c..0000000000 --- a/src/Form/WarningMessageType.php +++ /dev/null @@ -1,23 +0,0 @@ -setDefaults([ - 'mapped' => false, - 'required' => false, - ]); - } -} diff --git a/src/Migrations/Version20241105132620.php b/src/Migrations/Version20241105132620.php new file mode 100644 index 0000000000..b812f6ebad --- /dev/null +++ b/src/Migrations/Version20241105132620.php @@ -0,0 +1,36 @@ +getAllDomainIds() as $domainId) { + $this->sql('INSERT INTO setting_values (name, domain_id, value, type) VALUES (:name, :domainId, :value, :type)', [ + 'domainId' => $domainId, + 'name' => 'feedDeliveryDaysForOutOfStockProducts', + 'value' => -1, + 'type' => 'integer', + ]); + } + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Migrations/Version20241113124734.php b/src/Migrations/Version20241113124734.php new file mode 100644 index 0000000000..fc810d2317 --- /dev/null +++ b/src/Migrations/Version20241113124734.php @@ -0,0 +1,32 @@ +isAppMigrationNotInstalledRemoveIfExists('Version20200114084117')) { + $this->sql('ALTER TABLE product_translations ADD name_prefix VARCHAR(255) DEFAULT NULL'); + $this->sql('ALTER TABLE product_translations ALTER name_prefix DROP DEFAULT'); + + $this->sql('ALTER TABLE product_translations ADD name_sufix VARCHAR(255) DEFAULT NULL'); + $this->sql('ALTER TABLE product_translations ALTER name_sufix DROP DEFAULT'); + } + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Migrations/Version20241113185044.php b/src/Migrations/Version20241113185044.php new file mode 100644 index 0000000000..4c17506929 --- /dev/null +++ b/src/Migrations/Version20241113185044.php @@ -0,0 +1,26 @@ +sql('ALTER TABLE product_translations RENAME COLUMN name_sufix TO name_suffix'); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Migrations/Version20241121094752.php b/src/Migrations/Version20241121094752.php new file mode 100644 index 0000000000..88a93c21fd --- /dev/null +++ b/src/Migrations/Version20241121094752.php @@ -0,0 +1,26 @@ +sql('UPDATE product_stocks SET product_quantity = 0 WHERE product_id IN (SELECT id FROM products WHERE variant_type = \'main\')'); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + */ + public function down(Schema $schema): void + { + } +} diff --git a/src/Model/Cart/CartFacade.php b/src/Model/Cart/CartFacade.php index f2310a6417..44059ca982 100644 --- a/src/Model/Cart/CartFacade.php +++ b/src/Model/Cart/CartFacade.php @@ -64,9 +64,6 @@ public function addProductToExistingCart( Cart $cart, bool $isAbsoluteQuantity = false, ): AddProductResult { - $maximumOrderQuantity = $this->productAvailabilityFacade->getGroupedStockQuantityByProductAndDomainId($product, $this->domain->getId()); - $notOnStockQuantity = 0; - if (!is_int($quantity) || $quantity <= 0) { throw new InvalidQuantityException($quantity); } @@ -81,11 +78,7 @@ public function addProductToExistingCart( $addedQuantity = $quantity; - if ($newQuantity > $maximumOrderQuantity) { - $notOnStockQuantity = $newQuantity - $maximumOrderQuantity; - $newQuantity = $maximumOrderQuantity; - $addedQuantity = $quantity - $notOnStockQuantity; - } + $notOnStockQuantity = $this->productAvailabilityFacade->getNotOnStockQuantity($product, $this->domain->getId(), $newQuantity) ?? 0; $item->changeQuantity($newQuantity); $item->changeAddedAt(new DateTime()); $result = new AddProductResult($item, false, $addedQuantity, $notOnStockQuantity); @@ -96,12 +89,8 @@ public function addProductToExistingCart( } } - if ($quantity > $maximumOrderQuantity) { - $notOnStockQuantity = $quantity - $maximumOrderQuantity; - $quantity = $maximumOrderQuantity; - } - $productPrice = $this->productPriceCalculation->calculatePriceForCurrentUser($product); + $notOnStockQuantity = $this->productAvailabilityFacade->getNotOnStockQuantity($product, $this->domain->getId(), $quantity) ?? 0; $newCartItem = $this->cartItemFactory->create($cart, $product, $quantity, $productPrice->getPriceWithVat()); $cart->addItem($newCartItem); $cart->setModifiedNow(); diff --git a/src/Model/Cart/CartMigrationFacade.php b/src/Model/Cart/CartMigrationFacade.php deleted file mode 100644 index 735ddefafc..0000000000 --- a/src/Model/Cart/CartMigrationFacade.php +++ /dev/null @@ -1,90 +0,0 @@ -customerUserIdentifierFactory->get(); - $currentCart = $this->cartFacade->getCartByCustomerUserIdentifierCreateIfNotExists($customerUserIdentifier); - - foreach ($cart->getItems() as $itemToMerge) { - $similarItem = $currentCart->findSimilarItemByItem($itemToMerge); - - if ($similarItem instanceof CartItem) { - $similarItem->changeQuantity($similarItem->getQuantity() + $itemToMerge->getQuantity()); - } else { - $newCartItem = $this->cartItemFactory->create( - $currentCart, - $itemToMerge->getProduct(), - $itemToMerge->getQuantity(), - $itemToMerge->getWatchedPrice(), - ); - $currentCart->addItem($newCartItem); - } - } - $currentCart->setModifiedNow(); - - foreach ($currentCart->getItems() as $item) { - $this->em->persist($item); - } - - $this->cartFacade->deleteCart($cart); - - $this->em->flush(); - } - - /** - * @param \Symfony\Component\HttpKernel\Event\ControllerEvent $event - */ - public function onKernelController(ControllerEvent $event): void - { - $session = $event->getRequest()->getSession(); - - $previousCartIdentifier = $session->get(static::SESSION_PREVIOUS_CART_IDENTIFIER); - - if ( - $previousCartIdentifier !== null - && $previousCartIdentifier !== '' - && $previousCartIdentifier !== $session->getId() - ) { - $previousCustomerUserIdentifier = $this->customerUserIdentifierFactory->getOnlyWithCartIdentifier( - $previousCartIdentifier, - ); - $cart = $this->cartFacade->findCartByCustomerUserIdentifier($previousCustomerUserIdentifier); - - if ($cart !== null) { - $this->mergeCurrentCartWithCart($cart); - } - } - $session->set(static::SESSION_PREVIOUS_CART_IDENTIFIER, $session->getId()); - } -} diff --git a/src/Model/Order/Processing/OrderProcessorMiddleware/AddProductsMiddleware.php b/src/Model/Order/Processing/OrderProcessorMiddleware/AddProductsMiddleware.php index 02a25e41ad..120060de18 100644 --- a/src/Model/Order/Processing/OrderProcessorMiddleware/AddProductsMiddleware.php +++ b/src/Model/Order/Processing/OrderProcessorMiddleware/AddProductsMiddleware.php @@ -67,7 +67,7 @@ protected function createProductItemData( $orderItemData = $this->orderItemDataFactory->create(OrderItemTypeEnum::TYPE_PRODUCT); - $orderItemData->name = $product->getName($locale); + $orderItemData->name = $product->getFullName($locale); $orderItemData->setUnitPrice($quantifiedItemPrice->getUnitPrice()); $orderItemData->setTotalPrice($quantifiedItemPrice->getTotalPrice()); $orderItemData->vatPercent = $quantifiedItemPrice->getVat()->getPercent(); diff --git a/src/Model/Product/Availability/ProductAvailabilityFacade.php b/src/Model/Product/Availability/ProductAvailabilityFacade.php index d641a2de0c..1b079104e2 100644 --- a/src/Model/Product/Availability/ProductAvailabilityFacade.php +++ b/src/Model/Product/Availability/ProductAvailabilityFacade.php @@ -53,15 +53,15 @@ public function getProductAvailabilityInformationByDomainId(Product $product, in /** * @param \Shopsys\FrameworkBundle\Model\Product\Product $product * @param int $domainId - * @return int|null + * @return int */ - public function getProductAvailabilityDaysByDomainId(Product $product, int $domainId): ?int + public function getProductAvailabilityDaysForFeedsByDomainId(Product $product, int $domainId): int { if ($this->isProductAvailableOnDomainCached($product, $domainId)) { return 0; } - return null; + return $this->setting->getForDomain(Setting::FEED_DELIVERY_DAYS_FOR_OUT_OF_STOCK_PRODUCTS, $domainId); } /** @@ -83,25 +83,14 @@ public function getProductAvailabilityStatusByDomainId( /** * @param \Shopsys\FrameworkBundle\Model\Product\Product $product * @param int $domainId - * @return string + * @return int|null */ - public function getProductAvailableStoresCountInformationByDomainId(Product $product, int $domainId): string + public function getAvailableStoresCount(Product $product, int $domainId): ?int { - $count = $this->getAvailableStoresCount($product, $domainId); - - return t( - '{0}|{1}Available in %count% store|[2,Inf]Available in %count% stores', - ['%count%' => $count], - ); - } + if ($product->isMainVariant()) { + return null; + } - /** - * @param \Shopsys\FrameworkBundle\Model\Product\Product $product - * @param int $domainId - * @return int - */ - public function getAvailableStoresCount(Product $product, int $domainId): int - { $productStocks = $this->productStockFacade->getProductStocksByProduct($product); $count = 0; @@ -139,6 +128,10 @@ public function getProductStoresAvailabilitiesInformationByDomainIdIndexedByStor Product $product, int $domainId, ): array { + if ($product->isMainVariant()) { + return []; + } + $stores = $this->storeFacade->getStoresByDomainId($domainId); $isAvailable = $this->isProductAvailableOnDomainCached($product, $domainId); @@ -229,10 +222,14 @@ public function getTransferDaysByDomainId(int $domainId): int /** * @param \Shopsys\FrameworkBundle\Model\Product\Product $product * @param int $domainId - * @return int + * @return int|null */ - public function getGroupedStockQuantityByProductAndDomainId(Product $product, int $domainId): int + public function getGroupedStockQuantityByProductAndDomainId(Product $product, int $domainId): ?int { + if ($product->isMainVariant()) { + return null; + } + $productStocksByDomainIdIndexedByStockId = $this->productStockFacade->getProductStocksByProductAndDomainIdIndexedByStockId($product, $domainId); return $this->sumProductStockQuantities($productStocksByDomainIdIndexedByStockId); @@ -270,4 +267,26 @@ public function getOutOfStockText(string $domainLocale): string { return t('Out of stock', [], Translator::DEFAULT_TRANSLATION_DOMAIN, $domainLocale); } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Product $product + * @param int $domainId + * @param int $quantityToAdd + * @return int|null + */ + public function getNotOnStockQuantity(Product $product, int $domainId, int $quantityToAdd): ?int + { + if ($product->isMainVariant()) { + return null; + } + + $notOnStockQuantity = 0; + $productTotalQuantity = $this->getGroupedStockQuantityByProductAndDomainId($product, $domainId); + + if ($quantityToAdd > $productTotalQuantity) { + $notOnStockQuantity = $quantityToAdd - $productTotalQuantity; + } + + return $notOnStockQuantity; + } } diff --git a/src/Model/Product/Elasticsearch/ProductExportRepository.php b/src/Model/Product/Elasticsearch/ProductExportRepository.php index efe2d477d1..139d72bf2a 100644 --- a/src/Model/Product/Elasticsearch/ProductExportRepository.php +++ b/src/Model/Product/Elasticsearch/ProductExportRepository.php @@ -153,6 +153,8 @@ protected function getExportedFieldValue(int $domainId, Product $product, string ProductExportFieldProvider::PARTNO => $product->getPartno(), ProductExportFieldProvider::EAN => $product->getEan(), ProductExportFieldProvider::NAME => $product->getName($locale), + ProductExportFieldProvider::NAME_PREFIX => $product->getNamePrefix($locale), + ProductExportFieldProvider::NAME_SUFFIX => $product->getNameSuffix($locale), ProductExportFieldProvider::DESCRIPTION => $product->getDescription($domainId), ProductExportFieldProvider::SHORT_DESCRIPTION => $product->getShortDescription($domainId), ProductExportFieldProvider::BRAND => $product->getBrand() ? $product->getBrand()->getId() : '', @@ -171,7 +173,6 @@ protected function getExportedFieldValue(int $domainId, Product $product, string ProductExportFieldProvider::CALCULATED_SELLING_DENIED => $product->getCalculatedSellingDenied(), ProductExportFieldProvider::SELLING_DENIED => $product->isSellingDenied(), ProductExportFieldProvider::AVAILABILITY => $this->productAvailabilityFacade->getProductAvailabilityInformationByDomainId($product, $domainId), - ProductExportFieldProvider::AVAILABILITY_DISPATCH_TIME => $this->productAvailabilityFacade->getProductAvailabilityDaysByDomainId($product, $domainId), ProductExportFieldProvider::IS_MAIN_VARIANT => $product->isMainVariant(), ProductExportFieldProvider::IS_VARIANT => $product->isVariant(), ProductExportFieldProvider::DETAIL_URL => $this->extractDetailUrl($domainId, $product), @@ -188,6 +189,9 @@ protected function getExportedFieldValue(int $domainId, Product $product, string ProductExportFieldProvider::HREFLANG_LINKS => $this->hreflangLinksFacade->getForProduct($product, $domainId), ProductExportFieldProvider::PRODUCT_TYPE => $this->extractProductType($product, $domainId), ProductExportFieldProvider::PRIORITY_BY_PRODUCT_TYPE => $this->extractPriorityByProductType($product, $domainId), + ProductExportFieldProvider::AVAILABLE_STORES_COUNT => $this->productAvailabilityFacade->getAvailableStoresCount($product, $domainId), + ProductExportFieldProvider::STORE_AVAILABILITIES_INFORMATION => $this->extractStoreAvailabilitiesInformation($product, $domainId), + ProductExportFieldProvider::AVAILABILITY_STATUS => $this->productAvailabilityFacade->getProductAvailabilityStatusByDomainId($product, $domainId), default => throw new InvalidArgumentException(sprintf('There is no definition for exporting "%s" field to Elasticsearch', $field)), }; @@ -342,10 +346,12 @@ protected function extractPriorityByProductType(Product $product, int $domainId) { $productType = $this->extractProductType($product, $domainId); + $isProductAvailable = $this->productAvailabilityFacade->isProductAvailableOnDomainCached($product, $domainId); + return match ($productType) { - ProductTypeEnum::TYPE_BASIC => 20, - ProductTypeEnum::TYPE_INQUIRY => 10, - default => -100, + ProductTypeEnum::TYPE_BASIC => $isProductAvailable ? 20 : 5, + ProductTypeEnum::TYPE_INQUIRY => $isProductAvailable ? 10 : 0, + default => $isProductAvailable ? -100 : -200, }; } @@ -484,4 +490,27 @@ protected function reset(): void { $this->inMemoryCache->deleteAllItemsInNamespace(static::VARIANTS_CACHE_NAMESPACE); } + + /** + * @param \Shopsys\FrameworkBundle\Model\Product\Product $product + * @param int $domainId + * @return array + */ + protected function extractStoreAvailabilitiesInformation(Product $product, int $domainId): array + { + $storeAvailabilitiesInformation = $this->productAvailabilityFacade->getProductStoresAvailabilitiesInformationByDomainIdIndexedByStoreId($product, $domainId); + + $result = []; + + foreach ($storeAvailabilitiesInformation as $item) { + $result[] = [ + 'store_name' => $item->getStoreName(), + 'store_id' => $item->getStoreId(), + 'availability_information' => $item->getAvailabilityInformation(), + 'availability_status' => $item->getAvailabilityStatus(), + ]; + } + + return $result; + } } diff --git a/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php b/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php index 5c77b9efe0..921ba11d7c 100644 --- a/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php +++ b/src/Model/Product/Elasticsearch/Scope/ProductExportFieldProvider.php @@ -28,7 +28,6 @@ class ProductExportFieldProvider public const string CALCULATED_SELLING_DENIED = 'calculated_selling_denied'; public const string SELLING_DENIED = 'selling_denied'; public const string AVAILABILITY = 'availability'; - public const string AVAILABILITY_DISPATCH_TIME = 'availability_dispatch_time'; public const string IS_MAIN_VARIANT = 'is_main_variant'; public const string IS_VARIANT = 'is_variant'; public const string DETAIL_URL = 'detail_url'; @@ -45,6 +44,11 @@ class ProductExportFieldProvider public const string HREFLANG_LINKS = 'hreflang_links'; public const string PRODUCT_TYPE = 'product_type'; public const string PRIORITY_BY_PRODUCT_TYPE = 'priority_by_product_type'; + public const string NAME_PREFIX = 'name_prefix'; + public const string NAME_SUFFIX = 'name_suffix'; + public const string AVAILABLE_STORES_COUNT = 'available_stores_count'; + public const string STORE_AVAILABILITIES_INFORMATION = 'store_availabilities_information'; + public const string AVAILABILITY_STATUS = 'availability_status'; /** * @return string[] diff --git a/src/Model/Product/Elasticsearch/Scope/ProductExportScopeConfig.php b/src/Model/Product/Elasticsearch/Scope/ProductExportScopeConfig.php index 338f78166f..f54676484d 100644 --- a/src/Model/Product/Elasticsearch/Scope/ProductExportScopeConfig.php +++ b/src/Model/Product/Elasticsearch/Scope/ProductExportScopeConfig.php @@ -75,9 +75,12 @@ protected function loadProductExportScopeRules(): void ]); $this->addNewExportScopeRule(self::SCOPE_STOCKS, [ ProductExportFieldProvider::AVAILABILITY, - ProductExportFieldProvider::AVAILABILITY_DISPATCH_TIME, + ProductExportFieldProvider::PRIORITY_BY_PRODUCT_TYPE, ProductExportFieldProvider::IN_STOCK, ProductExportFieldProvider::STOCK_QUANTITY, + ProductExportFieldProvider::AVAILABLE_STORES_COUNT, + ProductExportFieldProvider::STORE_AVAILABILITIES_INFORMATION, + ProductExportFieldProvider::AVAILABILITY_STATUS, ]); $this->addNewExportScopeRule(self::SCOPE_URL, [ ProductExportFieldProvider::DETAIL_URL, @@ -93,6 +96,8 @@ protected function loadProductExportScopeRules(): void ProductExportFieldProvider::NAME, ProductExportFieldProvider::DETAIL_URL, ProductExportFieldProvider::HREFLANG_LINKS, + ProductExportFieldProvider::NAME_PREFIX, + ProductExportFieldProvider::NAME_SUFFIX, ], [ self::PRECONDITION_VISIBILITY_RECALCULATION, ]); diff --git a/src/Model/Product/Product.php b/src/Model/Product/Product.php index 8334aac13d..d3caab0411 100644 --- a/src/Model/Product/Product.php +++ b/src/Model/Product/Product.php @@ -293,17 +293,32 @@ public function getName($locale = null) /** * @return string[] */ - public function getFullnames() + public function getFullNames() { $fullNamesByLocale = []; foreach ($this->translations as $translation) { - $fullNamesByLocale[$translation->getLocale()] = $this->getName($translation->getLocale()); + $fullNamesByLocale[$translation->getLocale()] = $this->getFullName($translation->getLocale()); } return $fullNamesByLocale; } + /** + * @param string|null $locale + * @return string|null + */ + public function getFullName(?string $locale = null): ?string + { + return trim( + $this->getNamePrefix($locale) + . ' ' + . $this->getName($locale) + . ' ' + . $this->getNameSuffix($locale), + ); + } + /** * @param string|null $locale * @return string|null @@ -651,6 +666,14 @@ protected function setTranslations(ProductData $productData) foreach ($productData->variantAlias as $locale => $variantAlias) { $this->translation($locale)->setVariantAlias($variantAlias); } + + foreach ($productData->namePrefix as $locale => $namePrefix) { + $this->translation($locale)->setNamePrefix($namePrefix); + } + + foreach ($productData->nameSuffix as $locale => $nameSuffix) { + $this->translation($locale)->setNameSuffix($nameSuffix); + } } /** @@ -948,4 +971,22 @@ public function getExcludedTransports() { return $this->excludedTransports->getValues(); } + + /** + * @param string|null $locale + * @return string|null + */ + public function getNamePrefix($locale = null) + { + return $this->translation($locale)->getNamePrefix(); + } + + /** + * @param string|null $locale + * @return string|null + */ + public function getNameSuffix($locale = null) + { + return $this->translation($locale)->getNameSuffix(); + } } diff --git a/src/Model/Product/ProductData.php b/src/Model/Product/ProductData.php index 16f182439e..f4f9fc79a4 100644 --- a/src/Model/Product/ProductData.php +++ b/src/Model/Product/ProductData.php @@ -13,6 +13,16 @@ class ProductData */ public $name; + /** + * @var string[]|null[] + */ + public $namePrefix; + + /** + * @var string[]|null[] + */ + public $nameSuffix; + /** * @var string|null */ @@ -206,6 +216,8 @@ class ProductData public function __construct() { $this->name = []; + $this->namePrefix = []; + $this->nameSuffix = []; $this->sellingDenied = false; $this->hidden = false; $this->flagsByDomainId = []; diff --git a/src/Model/Product/ProductDataFactory.php b/src/Model/Product/ProductDataFactory.php index 8aaa10980a..73866a64bc 100644 --- a/src/Model/Product/ProductDataFactory.php +++ b/src/Model/Product/ProductDataFactory.php @@ -108,6 +108,8 @@ protected function fillNew(ProductData $productData): void foreach ($this->domain->getAllLocales() as $locale) { $productData->name[$locale] = null; $productData->variantAlias[$locale] = null; + $productData->namePrefix[$locale] = null; + $productData->nameSuffix[$locale] = null; } $this->fillProductStockByStocks($productData); @@ -139,6 +141,8 @@ protected function fillFromProduct(ProductData $productData, Product $product): $productData->name[$locale] = $translation->getName(); $productData->variantAlias[$locale] = $translation->getVariantAlias(); + $productData->namePrefix[$locale] = $translation->getNamePrefix(); + $productData->nameSuffix[$locale] = $translation->getNameSuffix(); } foreach ($this->domain->getAllIds() as $domainId) { diff --git a/src/Model/Product/ProductFacade.php b/src/Model/Product/ProductFacade.php index f29dd6f176..b03983073d 100644 --- a/src/Model/Product/ProductFacade.php +++ b/src/Model/Product/ProductFacade.php @@ -132,7 +132,7 @@ public function setAdditionalDataAfterCreate(Product $product, ProductData $prod $this->uploadedFileFacade->manageFiles($product, $productData->files); $this->friendlyUrlFacade->saveUrlListFormData('front_product_detail', $product->getId(), $productData->urls); - $this->friendlyUrlFacade->createFriendlyUrls('front_product_detail', $product->getId(), $product->getFullnames()); + $this->friendlyUrlFacade->createFriendlyUrls('front_product_detail', $product->getId(), $product->getFullNames()); } /** @@ -147,7 +147,7 @@ public function edit( string $priority = ProductRecalculationPriorityEnum::REGULAR, ): Product { $product = $this->productRepository->getById($productId); - $originalNames = $product->getFullnames(); + $originalNames = $product->getFullNames(); $productCategoryDomains = $this->productCategoryDomainFactory->createMultiple( $product, @@ -419,7 +419,7 @@ protected function getChangedNamesByLocale(Product $product, array $originalName { $changedProductNames = []; - foreach ($product->getFullnames() as $locale => $name) { + foreach ($product->getFullNames() as $locale => $name) { if ($name !== null && $name !== $originalNames[$locale]) { $changedProductNames[$locale] = $name; } diff --git a/src/Model/Product/ProductRepository.php b/src/Model/Product/ProductRepository.php index f7e50c4ad4..9b0d68eee1 100644 --- a/src/Model/Product/ProductRepository.php +++ b/src/Model/Product/ProductRepository.php @@ -73,6 +73,20 @@ public function getAllSellableQueryBuilder($domainId, PricingGroup $pricingGroup return $queryBuilder; } + /** + * @param int $domainId + * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup + * @return \Doctrine\ORM\QueryBuilder + */ + public function getAllSellableWithoutInquiriesQueryBuilder(int $domainId, PricingGroup $pricingGroup): QueryBuilder + { + $queryBuilder = $this->getAllSellableQueryBuilder($domainId, $pricingGroup); + $queryBuilder->andWhere('p.productType != :inquiryProductType') + ->setParameter('inquiryProductType', ProductTypeEnum::TYPE_INQUIRY); + + return $queryBuilder; + } + /** * @param int $domainId * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup @@ -87,7 +101,6 @@ public function getAllOfferedQueryBuilder($domainId, PricingGroup $pricingGroup) ->setParameter('domainId', $domainId); $queryBuilder - ->andWhere('pd.saleExclusion = FALSE') ->andWhere('p.calculatedSellingDenied = FALSE'); return $queryBuilder; @@ -115,20 +128,6 @@ public function getAllVisibleQueryBuilder($domainId, PricingGroup $pricingGroup) return $queryBuilder; } - /** - * @param int $domainId - * @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroup $pricingGroup - * @return \Doctrine\ORM\QueryBuilder - */ - public function getAllVisibleWithoutInquiriesQueryBuilder(int $domainId, PricingGroup $pricingGroup): QueryBuilder - { - $queryBuilder = $this->getAllVisibleQueryBuilder($domainId, $pricingGroup); - $queryBuilder->andWhere('p.productType != :inquiryProductType') - ->setParameter('inquiryProductType', ProductTypeEnum::TYPE_INQUIRY); - - return $queryBuilder; - } - /** * @param \Doctrine\ORM\QueryBuilder $queryBuilder * @param string $locale diff --git a/src/Model/Product/ProductSellingDeniedRecalculator.php b/src/Model/Product/ProductSellingDeniedRecalculator.php index ff8e4766ab..e96dc0403a 100644 --- a/src/Model/Product/ProductSellingDeniedRecalculator.php +++ b/src/Model/Product/ProductSellingDeniedRecalculator.php @@ -73,20 +73,6 @@ protected function calculatePerDomain(array $productIds): void p.calculated_selling_denied = TRUE OR pd.domain_hidden = TRUE - OR ( - p.variant_type != :variantTypeMain - AND - p.product_type != :inquiryProductType - AND - NOT EXISTS( - SELECT 1 - FROM product_stocks as ps - JOIN stocks as s ON s.id = ps.stock_id - JOIN stock_domains sd ON s.id = sd.stock_id AND sd.domain_id = :domainId - WHERE ps.product_id = p.id AND sd.is_enabled = TRUE - HAVING SUM(ps.product_quantity) > 0 - ) - ) OR ( pd.sale_exclusion = TRUE AND diff --git a/src/Model/Product/ProductTranslation.php b/src/Model/Product/ProductTranslation.php index b5d76b9896..ba45b6665a 100644 --- a/src/Model/Product/ProductTranslation.php +++ b/src/Model/Product/ProductTranslation.php @@ -34,6 +34,18 @@ class ProductTranslation extends AbstractTranslation */ protected $variantAlias; + /** + * @var string|null + * @ORM\Column(type="string", length=255, nullable=true) + */ + protected $namePrefix; + + /** + * @var string|null + * @ORM\Column(type="string", length=255, nullable=true) + */ + protected $nameSuffix; + /** * @return string|null */ @@ -65,4 +77,36 @@ public function setVariantAlias($variantAlias) { $this->variantAlias = TransformString::getTrimmedStringOrNullOnEmpty($variantAlias); } + + /** + * @return string|null + */ + public function getNamePrefix() + { + return $this->namePrefix; + } + + /** + * @param string|null $namePrefix + */ + public function setNamePrefix($namePrefix) + { + $this->namePrefix = $namePrefix; + } + + /** + * @return string|null + */ + public function getNameSuffix() + { + return $this->nameSuffix; + } + + /** + * @param string|null $nameSuffix + */ + public function setNameSuffix($nameSuffix) + { + $this->nameSuffix = $nameSuffix; + } } diff --git a/src/Model/Product/Search/FilterQuery.php b/src/Model/Product/Search/FilterQuery.php index ad1fc50c0f..431f8dec05 100644 --- a/src/Model/Product/Search/FilterQuery.php +++ b/src/Model/Product/Search/FilterQuery.php @@ -69,11 +69,6 @@ public function applyOrdering(string $orderingModeId, PricingGroup $pricingGroup return $clone; } - $clone->sorting['availability_dispatch_time'] = [ - 'order' => 'asc', - 'missing' => '_last', - ]; - if ($orderingModeId === ProductListOrderingConfig::ORDER_BY_PRIORITY) { $clone->sorting['priority_by_product_type'] = 'desc'; $clone->sorting['ordering_priority'] = 'desc'; @@ -154,13 +149,12 @@ public function applyOrdering(string $orderingModeId, PricingGroup $pricingGroup /** * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery */ - public function applyDefaultOrdering(): self + public function applyOrderingByIdAscending(): self { $clone = clone $this; $clone->sorting = [ - 'ordering_priority' => 'desc', - 'name.keyword' => 'asc', + 'id' => 'asc', ]; return $clone; diff --git a/src/Model/Product/Search/ProductElasticsearchConverter.php b/src/Model/Product/Search/ProductElasticsearchConverter.php index 3f0fab70fc..542e9c2264 100644 --- a/src/Model/Product/Search/ProductElasticsearchConverter.php +++ b/src/Model/Product/Search/ProductElasticsearchConverter.php @@ -4,6 +4,7 @@ namespace Shopsys\FrameworkBundle\Model\Product\Search; +use Shopsys\FrameworkBundle\Model\Product\Elasticsearch\Scope\ProductExportFieldProvider; use Shopsys\FrameworkBundle\Model\Product\ProductTypeEnum; class ProductElasticsearchConverter @@ -16,46 +17,57 @@ public function fillEmptyFields(array $product): array { $result = $product; - $result['id'] = $product['id'] ?? 0; - $result['availability'] = $product['availability'] ?? ''; - $result['catnum'] = $product['catnum'] ?? ''; - $result['description'] = $product['description'] ?? ''; - $result['detail_url'] = $product['detail_url'] ?? ''; - $result['ean'] = $product['ean'] ?? ''; - $result['name'] = $product['name'] ?? ''; - $result['partno'] = $product['partno'] ?? ''; - $result['short_description'] = $product['short_description'] ?? ''; - - $result['categories'] = $product['categories'] ?? []; - $result['flags'] = $product['flags'] ?? []; - $result['parameters'] = array_key_exists('parameters', $product) && $product['parameters'] ? $this->fillEmptyParameters($product['parameters']) : []; - $result['prices'] = $product['prices'] ?? []; - $result['visibility'] = $product['visibility'] ?? []; - $result['accessories'] = $product['accessories'] ?? []; - - $result['ordering_priority'] = $product['ordering_priority'] ?? 0; - - $result['in_stock'] = $product['in_stock'] ?? false; - $result['is_main_variant'] = $product['is_main_variant'] ?? false; - $result['is_variant'] = $product['is_variant'] ?? false; - $result['main_variant_id'] = $product['main_variant_id'] ?? null; - $result['variants'] = $product['variants'] ?? []; - - $result['calculated_selling_denied'] = $product['calculated_selling_denied'] ?? true; - $result['selling_denied'] = $product['selling_denied'] ?? true; + $result[ProductExportFieldProvider::ID] = $product[ProductExportFieldProvider::ID] ?? 0; + $result[ProductExportFieldProvider::AVAILABILITY] = $product[ProductExportFieldProvider::AVAILABILITY] ?? ''; + $result[ProductExportFieldProvider::CATNUM] = $product[ProductExportFieldProvider::CATNUM] ?? ''; + $result[ProductExportFieldProvider::DESCRIPTION] = $product[ProductExportFieldProvider::DESCRIPTION] ?? ''; + $result[ProductExportFieldProvider::DETAIL_URL] = $product[ProductExportFieldProvider::DETAIL_URL] ?? ''; + $result[ProductExportFieldProvider::EAN] = $product[ProductExportFieldProvider::EAN] ?? ''; + $result[ProductExportFieldProvider::NAME] = $product[ProductExportFieldProvider::NAME] ?? ''; + $result[ProductExportFieldProvider::PARTNO] = $product[ProductExportFieldProvider::PARTNO] ?? ''; + $result[ProductExportFieldProvider::SHORT_DESCRIPTION] = $product[ProductExportFieldProvider::SHORT_DESCRIPTION] ?? ''; + + $result[ProductExportFieldProvider::CATEGORIES] = $product[ProductExportFieldProvider::CATEGORIES] ?? []; + $result[ProductExportFieldProvider::FLAGS] = $product[ProductExportFieldProvider::FLAGS] ?? []; + $result[ProductExportFieldProvider::PARAMETERS] = array_key_exists(ProductExportFieldProvider::PARAMETERS, $product) && $product[ProductExportFieldProvider::PARAMETERS] ? $this->fillEmptyParameters($product[ProductExportFieldProvider::PARAMETERS]) : []; + $result[ProductExportFieldProvider::PRICES] = $product[ProductExportFieldProvider::PRICES] ?? []; + $result[ProductExportFieldProvider::VISIBILITY] = $product[ProductExportFieldProvider::VISIBILITY] ?? []; + $result[ProductExportFieldProvider::ACCESSORIES] = $product[ProductExportFieldProvider::ACCESSORIES] ?? []; + + $result[ProductExportFieldProvider::ORDERING_PRIORITY] = $product[ProductExportFieldProvider::ORDERING_PRIORITY] ?? 0; + + $result[ProductExportFieldProvider::IN_STOCK] = $product[ProductExportFieldProvider::IN_STOCK] ?? false; + $result[ProductExportFieldProvider::IS_MAIN_VARIANT] = $product[ProductExportFieldProvider::IS_MAIN_VARIANT] ?? false; + $result[ProductExportFieldProvider::IS_VARIANT] = $product[ProductExportFieldProvider::IS_VARIANT] ?? false; + $result[ProductExportFieldProvider::MAIN_VARIANT_ID] = $product[ProductExportFieldProvider::MAIN_VARIANT_ID] ?? null; + $result[ProductExportFieldProvider::VARIANTS] = $product[ProductExportFieldProvider::VARIANTS] ?? []; + + $result[ProductExportFieldProvider::CALCULATED_SELLING_DENIED] = $product[ProductExportFieldProvider::CALCULATED_SELLING_DENIED] ?? true; + $result[ProductExportFieldProvider::SELLING_DENIED] = $product[ProductExportFieldProvider::SELLING_DENIED] ?? true; // unknown default value, used for filtering only - $result['brand'] = $product['brand'] ?? null; - $result['brand_name'] = $product['brand_name'] ?? ''; - $result['brand_url'] = $product['brand_url'] ?? ''; - $result['main_category_id'] = $product['main_category_id'] ?? null; - - $result['seo_h1'] = $product['seo_h1'] ?? null; - $result['seo_title'] = $product['seo_title'] ?? null; - $result['seo_meta_description'] = $product['seo_meta_description'] ?? null; - $result['hreflang_links'] = $product['hreflang_links'] ?? []; - $result['product_type'] = $product['product_type'] ?? ProductTypeEnum::TYPE_BASIC; - $result['priority_by_product_type'] = $product['priority_by_product_type'] ?? 0; + $result[ProductExportFieldProvider::BRAND] = $product[ProductExportFieldProvider::BRAND] ?? null; + $result[ProductExportFieldProvider::BRAND_NAME] = $product[ProductExportFieldProvider::BRAND_NAME] ?? ''; + $result[ProductExportFieldProvider::BRAND_URL] = $product[ProductExportFieldProvider::BRAND_URL] ?? ''; + $result[ProductExportFieldProvider::MAIN_CATEGORY_ID] = $product[ProductExportFieldProvider::MAIN_CATEGORY_ID] ?? null; + + $result[ProductExportFieldProvider::SEO_H1] = $product[ProductExportFieldProvider::SEO_H1] ?? null; + $result[ProductExportFieldProvider::SEO_TITLE] = $product[ProductExportFieldProvider::SEO_TITLE] ?? null; + $result[ProductExportFieldProvider::SEO_META_DESCRIPTION] = $product[ProductExportFieldProvider::SEO_META_DESCRIPTION] ?? null; + $result[ProductExportFieldProvider::HREFLANG_LINKS] = $product[ProductExportFieldProvider::HREFLANG_LINKS] ?? []; + $result[ProductExportFieldProvider::PRODUCT_TYPE] = $product[ProductExportFieldProvider::PRODUCT_TYPE] ?? ProductTypeEnum::TYPE_BASIC; + $result[ProductExportFieldProvider::PRIORITY_BY_PRODUCT_TYPE] = $product[ProductExportFieldProvider::PRIORITY_BY_PRODUCT_TYPE] ?? 0; + + $result[ProductExportFieldProvider::NAME_PREFIX] = $product[ProductExportFieldProvider::NAME_PREFIX] ?? null; + $result[ProductExportFieldProvider::NAME_SUFFIX] = $product[ProductExportFieldProvider::NAME_SUFFIX] ?? $product['name_sufix'] ?? null; + + $result[ProductExportFieldProvider::AVAILABILITY_STATUS] = $product[ProductExportFieldProvider::AVAILABILITY_STATUS] ?? ''; + $result[ProductExportFieldProvider::STORE_AVAILABILITIES_INFORMATION] = $product[ProductExportFieldProvider::STORE_AVAILABILITIES_INFORMATION] ?? []; + $result[ProductExportFieldProvider::AVAILABLE_STORES_COUNT] = $product[ProductExportFieldProvider::AVAILABLE_STORES_COUNT] ?? null; + $result[ProductExportFieldProvider::STOCK_QUANTITY] = $product[ProductExportFieldProvider::STOCK_QUANTITY] ?? null; + + $result[ProductExportFieldProvider::UUID] = $product[ProductExportFieldProvider::UUID] ?? '00000000-0000-0000-0000-000000000000'; + $result[ProductExportFieldProvider::UNIT] = $product[ProductExportFieldProvider::UNIT] ?? ''; return $result; } diff --git a/src/Model/Sitemap/SitemapRepository.php b/src/Model/Sitemap/SitemapRepository.php index a01f19697f..d0b5e2dfcc 100644 --- a/src/Model/Sitemap/SitemapRepository.php +++ b/src/Model/Sitemap/SitemapRepository.php @@ -17,8 +17,6 @@ use Shopsys\FrameworkBundle\Model\Product\Flag\FlagRepository; use Shopsys\FrameworkBundle\Model\Product\Product; use Shopsys\FrameworkBundle\Model\Product\ProductRepository; -use Shopsys\FrameworkBundle\Model\Stock\ProductStock; -use Shopsys\FrameworkBundle\Model\Stock\StockDomain; class SitemapRepository { @@ -57,6 +55,7 @@ public function getSitemapItemsForListableProducts(DomainConfig $domainConfig, P AND fu.domainId = :domainId AND fu.main = TRUE', ) + ->andWhere('p.calculatedSellingDenied = FALSE') ->setParameter('productDetailRouteName', 'front_product_detail') ->setParameter('domainId', $domainConfig->getId()); @@ -177,20 +176,11 @@ public function getSitemapItemsForSoldOutProducts(DomainConfig $domainConfig, Pr AND fu.main = TRUE', ) ->andWhere('p.variantType != :variantTypeMain') + ->andWhere('p.calculatedSellingDenied = TRUE') ->setParameter('variantTypeMain', Product::VARIANT_TYPE_MAIN) ->setParameter('productDetailRouteName', 'front_product_detail') ->setParameter('domainId', $domainConfig->getId()); - $subquery = $queryBuilder->getEntityManager()->createQueryBuilder() - ->select('1') - ->from(ProductStock::class, 'ps') - ->join(StockDomain::class, 'sd', Join::WITH, 'ps.stock = sd.stock AND sd.domainId = :domainId') - ->where('ps.product = p') - ->having('SUM(ps.productQuantity) = 0'); - - $this->productRepository->addDomain($queryBuilder, $domainConfig->getId()); - $queryBuilder->andWhere('EXISTS(' . $subquery->getDQL() . ') AND (pd.saleExclusion = true)'); - return $this->getSitemapItemsFromQueryBuilderWithSlugField($queryBuilder); } diff --git a/src/Model/Stock/ProductStockRepository.php b/src/Model/Stock/ProductStockRepository.php index f8a7fd2000..2421f5c160 100644 --- a/src/Model/Stock/ProductStockRepository.php +++ b/src/Model/Stock/ProductStockRepository.php @@ -110,7 +110,7 @@ public function isProductAvailableOnDomain(Product $product, int $domainId): boo ->select('CASE WHEN SUM(ps.productQuantity) > 0 THEN TRUE ELSE FALSE END'); if ($product->isMainVariant()) { - $queryBuilder->join(Product::class, 'p', Join::WITH, 'ps.product = p AND p.mainVariant = :product'); + $queryBuilder->join(Product::class, 'p', Join::WITH, 'ps.product = p AND p.mainVariant = :product AND p.calculatedSellingDenied = FALSE'); } else { $queryBuilder->where('ps.product = :product'); } diff --git a/src/Model/Stock/StockSettingsData.php b/src/Model/Stock/StockSettingsData.php index c9fd0b6b18..2487cf679c 100644 --- a/src/Model/Stock/StockSettingsData.php +++ b/src/Model/Stock/StockSettingsData.php @@ -10,4 +10,14 @@ class StockSettingsData * @var int|null */ public $transfer; + + /** + * @var array + */ + public $pluginData = []; + + /** + * @var int|null + */ + public $feedDeliveryDaysForOutOfStockProducts; } diff --git a/src/Model/Stock/StockSettingsDataFacade.php b/src/Model/Stock/StockSettingsDataFacade.php index a7e724f240..8cac1213f0 100644 --- a/src/Model/Stock/StockSettingsDataFacade.php +++ b/src/Model/Stock/StockSettingsDataFacade.php @@ -5,19 +5,24 @@ namespace Shopsys\FrameworkBundle\Model\Stock; use Shopsys\FrameworkBundle\Component\Domain\Config\DomainConfig; +use Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade; use Shopsys\FrameworkBundle\Component\Setting\Setting; use Shopsys\FrameworkBundle\Model\Product\Elasticsearch\Scope\ProductExportScopeConfig; use Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher; class StockSettingsDataFacade { + public const int PLUGIN_COMMON_ID = 0; + /** * @param \Shopsys\FrameworkBundle\Component\Setting\Setting $setting * @param \Shopsys\FrameworkBundle\Model\Product\Recalculation\ProductRecalculationDispatcher $productRecalculationDispatcher + * @param \Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade $pluginCrudExtensionFacade */ public function __construct( protected readonly Setting $setting, protected readonly ProductRecalculationDispatcher $productRecalculationDispatcher, + protected readonly PluginCrudExtensionFacade $pluginCrudExtensionFacade, ) { } @@ -27,10 +32,20 @@ public function __construct( */ public function edit(StockSettingsData $stockSettingsData, DomainConfig $domainConfig): void { + $domainId = $domainConfig->getId(); + $this->setting->setForDomain( Setting::TRANSFER_DAYS_BETWEEN_STOCKS, (int)$stockSettingsData->transfer, - $domainConfig->getId(), + $domainId, + ); + + $this->pluginCrudExtensionFacade->saveAllData('stockSettings', static::PLUGIN_COMMON_ID, $stockSettingsData->pluginData); + + $this->setting->setForDomain( + Setting::FEED_DELIVERY_DAYS_FOR_OUT_OF_STOCK_PRODUCTS, + $stockSettingsData->feedDeliveryDaysForOutOfStockProducts, + $domainId, ); $this->productRecalculationDispatcher->dispatchAllProducts([ProductExportScopeConfig::SCOPE_STOCKS]); diff --git a/src/Model/Stock/StockSettingsDataFactory.php b/src/Model/Stock/StockSettingsDataFactory.php index b6e82d4dd9..67f4793d65 100644 --- a/src/Model/Stock/StockSettingsDataFactory.php +++ b/src/Model/Stock/StockSettingsDataFactory.php @@ -4,15 +4,18 @@ namespace Shopsys\FrameworkBundle\Model\Stock; +use Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade; use Shopsys\FrameworkBundle\Component\Setting\Setting; class StockSettingsDataFactory { /** * @param \Shopsys\FrameworkBundle\Component\Setting\Setting $setting + * @param \Shopsys\FrameworkBundle\Component\Plugin\PluginCrudExtensionFacade $pluginCrudExtensionFacade */ public function __construct( protected readonly Setting $setting, + protected readonly PluginCrudExtensionFacade $pluginCrudExtensionFacade, ) { } @@ -24,6 +27,8 @@ public function getForDomainId(int $domainId): StockSettingsData { $settings = new StockSettingsData(); $settings->transfer = $this->setting->getForDomain(Setting::TRANSFER_DAYS_BETWEEN_STOCKS, $domainId); + $settings->pluginData = $this->pluginCrudExtensionFacade->getAllData('stockSettings', StockSettingsDataFacade::PLUGIN_COMMON_ID); + $settings->feedDeliveryDaysForOutOfStockProducts = $this->setting->getForDomain(Setting::FEED_DELIVERY_DAYS_FOR_OUT_OF_STOCK_PRODUCTS, $domainId); return $settings; } diff --git a/src/Resources/config/packages_registry.yaml b/src/Resources/config/packages_registry.yaml index 4414b1f12e..47f61d7d22 100644 --- a/src/Resources/config/packages_registry.yaml +++ b/src/Resources/config/packages_registry.yaml @@ -18,5 +18,6 @@ parameters: product-feed-heureka: { path: '%shopsys.framework.root_dir%/../product-feed-heureka', namespace: 'Shopsys\ProductFeed\HeurekaBundle', app_namespace: 'App\ProductFeed\Heureka' } product-feed-heureka-delivery: { path: '%shopsys.framework.root_dir%/../product-feed-heureka-delivery', namespace: 'Shopsys\ProductFeed\HeurekaDeliveryBundle', app_namespace: 'App\ProductFeed\HeurekaDelivery' } product-feed-luigis-box: {path: '%shopsys.framework.root_dir%/../product-feed-heureka-delivery', namespace: 'Shopsys\ProductFeed\LuigisBoxBundle', app_namespace: 'App\ProductFeed\LuigisBox' } + product-feed-mergado: { path: '%shopsys.framework.root_dir%/../product-feed-mergado', namespace: 'Shopsys\ProductFeed\MergadoBundle', app_namespace: 'App\ProductFeed\Mergado' } product-feed-zbozi: { path: '%shopsys.framework.root_dir%/../product-feed-zbozi', namespace: 'Shopsys\ProductFeed\ZboziBundle', app_namespace: 'App\ProductFeed\Zbozi' } s3-bridge: { path: '%shopsys.framework.root_dir%/../s3-bridge', namespace: 'Shopsys\S3Bridge', app_namespace: 'App\S3Bridge' } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 4e29701db1..0a49eaa7d4 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -641,10 +641,6 @@ services: tags: - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } - Shopsys\FrameworkBundle\Model\Cart\CartMigrationFacade: - tags: - - { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - Shopsys\FrameworkBundle\Model\Cart\Item\CartItemFactoryInterface: alias: Shopsys\FrameworkBundle\Model\Cart\Item\CartItemFactory diff --git a/src/Resources/translations/messages.cs.po b/src/Resources/translations/messages.cs.po index 2a78d94d0f..00487d1135 100644 --- a/src/Resources/translations/messages.cs.po +++ b/src/Resources/translations/messages.cs.po @@ -2440,12 +2440,18 @@ msgstr "Název ve výchozím jazyce není vyplněn" msgid "Name in the corresponding locale must be filled-in in order to display the file on the storefront" msgstr "Pro zobrazení souboru na doméně musí být vyplněn název v odpovídajícím jazyce" +msgid "Name prefix" +msgstr "Prefix názvu" + msgid "Name serves only for internal use within the administration" msgstr "Název slouží pouze pro interní použití v rámci administrace" msgid "Name serves only for internal use within the administration." msgstr "Název slouží pouze pro interní použití v rámci administrace." +msgid "Name suffix" +msgstr "Sufix názvu" + msgid "Names" msgstr "Názvy (web)" @@ -2707,6 +2713,9 @@ msgstr "Notifikační lišta - vše" msgid "Notification bar - view" msgstr "Notifikační lišta - zobrazení" +msgid "Number of delivery days for out of stock products in XML feeds" +msgstr "Počet dní k doručení neskladových položek pro XML feedy" + msgid "Number of generated promo codes" msgstr "Počet generovaných kupónů" @@ -2861,7 +2870,7 @@ msgid "Origin" msgstr "Původ" msgid "Out of stock" -msgstr "Vyprodáno" +msgstr "Aktuálně nedostupné" msgid "Package tracking" msgstr "Sledování zásilky" @@ -3655,6 +3664,9 @@ msgstr "Nastavit jako výchozí" msgid "Set prices manually" msgstr "Manuálně nastavené ceny" +msgid "Setting for domain \"%domainName%\" was saved." +msgstr "" + msgid "Settings" msgstr "Nastavení" @@ -3868,6 +3880,12 @@ msgstr "Optimální velikost ikony je 46x26 px. Je povolen pouze PNG formát." msgid "The product is no longer in the catalog. This was the catalog number at the time of the inquiry." msgstr "Produkt již není v katalogu. Toto bylo katalogové číslo v době vytvoření poptávky." +msgid "The stock quantities are set for the product variants separately." +msgstr "Skladové zásoby se nastavují zvlášť pro jednotlivé varianty." + +msgid "The value is used for: " +msgstr "Hodnota se používá pro: " + msgid "There are no parameter values of type color yet." msgstr "Žádná hodnota parametru typu barva zatím nebyla vytvořena." @@ -4264,9 +4282,6 @@ msgstr "Sklad {{ name }} byl nastaven jako výchozí." msgid "Warehouse overview" msgstr "Přehled skladů" -msgid "Warehouse setting %domainName% saved." -msgstr "Nastavení %domainName% skladů uloženo." - msgid "Warehouse settings" msgstr "Nastavení skladů" @@ -4303,6 +4318,9 @@ msgstr "XML Feedy" msgid "XML availability feeds" msgstr "XML dostupnostní feedy" +msgid "XML feeds settings" +msgstr "Nastavení XML feedů" + msgid "Yes" msgstr "Ano" @@ -4528,9 +4546,6 @@ msgstr "bez umístění" msgid "{0,1} Available in one week|[2,Inf] Available in %count% weeks" msgstr "{0,1} K dispozici za týden|[2,4] K dispozici za %count% týdny|[5,Inf] K dispozici za %count% týdnů" -msgid "{0}|{1}Available in %count% store|[2,Inf]Available in %count% stores" -msgstr "{0}|{1}Můžete mít ihned na %count% prodejně|[2,Inf]Můžete mít ihned na %count% prodejnách" - msgid "{1} There is one parameter slider that does not have its numeric values filled in.|[2,Inf] There are %count% parameter sliders that does not have its numeric values filled in." msgstr "{1} Existuje jeden parametr typu slider, který nemá vyplněné své číselné hodnoty.|[2,4] Existuje %count% parametrů typu slider, které nemají vyplněné své číselné hodnoty.|[5,Inf] Existuje %count% parametrů typu slider, které nemají vyplněné své číselné hodnoty." diff --git a/src/Resources/translations/messages.en.po b/src/Resources/translations/messages.en.po index d16bf9de32..ba7123c7de 100644 --- a/src/Resources/translations/messages.en.po +++ b/src/Resources/translations/messages.en.po @@ -2440,12 +2440,18 @@ msgstr "" msgid "Name in the corresponding locale must be filled-in in order to display the file on the storefront" msgstr "" +msgid "Name prefix" +msgstr "" + msgid "Name serves only for internal use within the administration" msgstr "" msgid "Name serves only for internal use within the administration." msgstr "" +msgid "Name suffix" +msgstr "" + msgid "Names" msgstr "" @@ -2707,6 +2713,9 @@ msgstr "" msgid "Notification bar - view" msgstr "" +msgid "Number of delivery days for out of stock products in XML feeds" +msgstr "" + msgid "Number of generated promo codes" msgstr "" @@ -3655,6 +3664,9 @@ msgstr "" msgid "Set prices manually" msgstr "" +msgid "Setting for domain \"%domainName%\" was saved." +msgstr "Nastavení pro doménu \"%domainName%\" bylo uloženo." + msgid "Settings" msgstr "" @@ -3868,6 +3880,12 @@ msgstr "" msgid "The product is no longer in the catalog. This was the catalog number at the time of the inquiry." msgstr "" +msgid "The stock quantities are set for the product variants separately." +msgstr "" + +msgid "The value is used for: " +msgstr "" + msgid "There are no parameter values of type color yet." msgstr "" @@ -4264,9 +4282,6 @@ msgstr "" msgid "Warehouse overview" msgstr "" -msgid "Warehouse setting %domainName% saved." -msgstr "" - msgid "Warehouse settings" msgstr "" @@ -4303,6 +4318,9 @@ msgstr "" msgid "XML availability feeds" msgstr "" +msgid "XML feeds settings" +msgstr "" + msgid "Yes" msgstr "" @@ -4528,9 +4546,6 @@ msgstr "" msgid "{0,1} Available in one week|[2,Inf] Available in %count% weeks" msgstr "" -msgid "{0}|{1}Available in %count% store|[2,Inf]Available in %count% stores" -msgstr "" - msgid "{1} There is one parameter slider that does not have its numeric values filled in.|[2,Inf] There are %count% parameter sliders that does not have its numeric values filled in." msgstr "" diff --git a/src/Resources/translations/validators.cs.po b/src/Resources/translations/validators.cs.po index 1d76d141b5..aeb4e391f8 100644 --- a/src/Resources/translations/validators.cs.po +++ b/src/Resources/translations/validators.cs.po @@ -409,6 +409,9 @@ msgstr "Vyplňte prosím telefon" msgid "Please enter the filename" msgstr "Prosím vyplňte název souboru" +msgid "Please enter the number of delivery days." +msgstr "Zadejte prosím počet dní k doručení" + msgid "Please enter the positive value." msgstr "Vyplňte prosím kladnou hodnotu." @@ -472,6 +475,12 @@ msgstr "Název produktu nesmí být delší než {{ limit }} znaků" msgid "Product parameters are duplicate." msgstr "Parametry produktů jsou duplicitní." +msgid "Product prefix name cannot be longer than {{ limit }} characters" +msgstr "Prefix produktu nesmí být delší než {{ limit }} znaků" + +msgid "Product suffix name cannot be longer than {{ limit }} characters" +msgstr "Suffix produktu nesmí být delší než {{ limit }} znaků" + msgid "Product with entered catalog number already exists" msgstr "Produkt s tímto katalogovým číslem již existuje" @@ -496,6 +505,9 @@ msgstr "Slevový kupón \"%promoCode%\" není platný pro žádný produkt v ko msgid "Promo code \"%promoCode%\" is not yet valid." msgstr "Slevový kupón \"%promoCode%\" není ještě možné uplatnit." +msgid "Quantity must be an integer" +msgstr "Množství musí být celé číslo" + msgid "Quantity must be greater than {{ compared_value }}" msgstr "Množství musí být větší než {{ compared_value }}" diff --git a/src/Resources/translations/validators.en.po b/src/Resources/translations/validators.en.po index 0f0fd5446f..2c1c82837b 100644 --- a/src/Resources/translations/validators.en.po +++ b/src/Resources/translations/validators.en.po @@ -409,6 +409,9 @@ msgstr "" msgid "Please enter the filename" msgstr "" +msgid "Please enter the number of delivery days." +msgstr "" + msgid "Please enter the positive value." msgstr "" @@ -472,6 +475,12 @@ msgstr "" msgid "Product parameters are duplicate." msgstr "" +msgid "Product prefix name cannot be longer than {{ limit }} characters" +msgstr "" + +msgid "Product suffix name cannot be longer than {{ limit }} characters" +msgstr "" + msgid "Product with entered catalog number already exists" msgstr "" @@ -496,6 +505,9 @@ msgstr "" msgid "Promo code \"%promoCode%\" is not yet valid." msgstr "" +msgid "Quantity must be an integer" +msgstr "" + msgid "Quantity must be greater than {{ compared_value }}" msgstr "" diff --git a/src/Resources/views/Admin/Content/Feed/feedDeliveryDaysForOutOfStockProductsInfo.html.twig b/src/Resources/views/Admin/Content/Feed/feedDeliveryDaysForOutOfStockProductsInfo.html.twig new file mode 100644 index 0000000000..4e11eb023d --- /dev/null +++ b/src/Resources/views/Admin/Content/Feed/feedDeliveryDaysForOutOfStockProductsInfo.html.twig @@ -0,0 +1,11 @@ +{% trans %} + The value is used for: + +{% endtrans %} diff --git a/src/Resources/views/Admin/Content/Stock/settings.html.twig b/src/Resources/views/Admin/Content/Stock/settings.html.twig index cc678d1893..df13c641dc 100644 --- a/src/Resources/views/Admin/Content/Stock/settings.html.twig +++ b/src/Resources/views/Admin/Content/Stock/settings.html.twig @@ -8,16 +8,7 @@ {{ render(controller('Shopsys\\FrameworkBundle\\Controller\\Admin\\DomainController::domainTabsAction')) }} {{ form_start(form) }} -
-
-
- {{ 'Warehouse settings'|trans }} -
-
- {{ form_row(form.transfer) }} -
-
-
+ {% embed '@!ShopsysFramework/Admin/Inline/FixedBar/fixedBar.html.twig' %} {% block fixed_bar_content %} {{ form_widget(form.save, { label: 'Save warehouse settings'|trans}) }} diff --git a/src/Resources/views/Admin/Form/message.html.twig b/src/Resources/views/Admin/Form/message.html.twig new file mode 100644 index 0000000000..3ea9b85af5 --- /dev/null +++ b/src/Resources/views/Admin/Form/message.html.twig @@ -0,0 +1,9 @@ +{% block message_label %}{% endblock message_label %} +{% block message_widget %} +
+
+ + {{ data|raw }} +
+
+{% endblock message_widget %} diff --git a/src/Resources/views/Admin/Form/warningMessage.html.twig b/src/Resources/views/Admin/Form/warningMessage.html.twig deleted file mode 100644 index 20f8a209f7..0000000000 --- a/src/Resources/views/Admin/Form/warningMessage.html.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% block warning_message_label %}{% endblock warning_message_label %} -{% block warning_message_widget %} -
-
- - {{ data }} -
-
-{% endblock warning_message_widget %} diff --git a/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php b/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php index 5ba1a1d797..371b2bb1e2 100644 --- a/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php +++ b/tests/Unit/Model/Product/Search/ProductElasticsearchConverterTest.php @@ -55,6 +55,14 @@ public function testFillEmptyFields(): void 'hreflang_links' => [], 'product_type' => 'basic', 'priority_by_product_type' => 0, + 'name_prefix' => null, + 'name_suffix' => null, + 'availability_status' => '', + 'store_availabilities_information' => [], + 'available_stores_count' => null, + 'stock_quantity' => null, + 'uuid' => '00000000-0000-0000-0000-000000000000', + 'unit' => '', ]; $converter = new ProductElasticsearchConverter(); @@ -122,6 +130,14 @@ public function testFillEmptyParameterFields(): void 'hreflang_links' => [], 'product_type' => 'basic', 'priority_by_product_type' => 0, + 'name_prefix' => null, + 'name_suffix' => null, + 'availability_status' => '', + 'store_availabilities_information' => [], + 'available_stores_count' => null, + 'stock_quantity' => null, + 'uuid' => '00000000-0000-0000-0000-000000000000', + 'unit' => '', ]; $converter = new ProductElasticsearchConverter();