Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@
- Added `\craft\commerce\services\Inventory::EVENT_AFTER_EXECUTE_INVENTORY_MOVEMENT`. ([#4063](https://github.com/craftcms/commerce/pull/4063))

### System

- Fixed a bug where purchasables could have a shipping category that was no longer available to their product type. ([#4018](https://github.com/craftcms/commerce/issues/4018))
15 changes: 10 additions & 5 deletions src/controllers/ShippingCategoriesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,16 @@ public function actionSave(): ?Response
$shippingCategory->default = (bool)$this->request->getBodyParam('default');

// Set the new product types
$postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: [];
$productTypes = [];
foreach ($postedProductTypes as $productTypeId) {
if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) {
$productTypes[] = $productType;
// If this is the default category, it should be available to all product types
if ($shippingCategory->default) {
$productTypes = Plugin::getInstance()->getProductTypes()->getAllProductTypes();
} else {
$postedProductTypes = $this->request->getBodyParam('productTypes', []) ?: [];
$productTypes = [];
foreach ($postedProductTypes as $productTypeId) {
if ($productTypeId && $productType = Plugin::getInstance()->getProductTypes()->getProductTypeById($productTypeId)) {
$productTypes[] = $productType;
}
}
}
$shippingCategory->setProductTypes($productTypes);
Expand Down
36 changes: 36 additions & 0 deletions src/services/ShippingCategories.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,42 @@ public function saveShippingCategory(ShippingCategory $shippingCategory, bool $r
// Newly set product types this shipping category is available to
$newProductTypeIds = ArrayHelper::getColumn($shippingCategory->getProductTypes(), 'id');

// Find product types that are being removed from this shipping category
$removedProductTypeIds = array_diff($currentProductTypeIds, $newProductTypeIds);

// Update purchasables to default shipping category when product types are removed
if (!empty($removedProductTypeIds)) {
$defaultShippingCategory = $this->getDefaultShippingCategory($shippingCategory->storeId);

// Get all variant purchasables that currently have this shipping category but whose product type is being removed
$purchasableIds = (new Query())
->select(['ps.purchasableId'])
->from(['ps' => Table::PURCHASABLES_STORES])
->innerJoin(['v' => Table::VARIANTS], '[[ps.purchasableId]] = [[v.id]]')
->innerJoin(['p' => Table::PRODUCTS], '[[v.primaryOwnerId]] = [[p.id]]')
->where([
'ps.shippingCategoryId' => $shippingCategory->id,
'ps.storeId' => $shippingCategory->storeId,
'p.typeId' => $removedProductTypeIds,
])
->column();

if (!empty($purchasableIds)) {
// Update these purchasables to use the default shipping category
Craft::$app->getDb()->createCommand()
->update(
Table::PURCHASABLES_STORES,
['shippingCategoryId' => $defaultShippingCategory->id],
[
'purchasableId' => $purchasableIds,
'storeId' => $shippingCategory->storeId,
'shippingCategoryId' => $shippingCategory->id,
]
)
->execute();
}
}

foreach ($currentProductTypeIds as $oldProductTypeId) {
// If we are removing a product type for this shipping category the products of that type should be re-saved
if (!in_array($oldProductTypeId, $newProductTypeIds, false)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,27 @@
{% set warning = "" %}
{% endif %}

{% set isDefault = shippingCategory is defined and shippingCategory.default %}
{% set productTypeValues = isDefault ? productTypesOptions|keys : (shippingCategory is defined ? shippingCategory.productTypeIds : []) %}

{{ forms.checkboxSelectField({
label: "Available to Product Types"|t('commerce'),
instructions: "Which product types should this category be available to?"|t('commerce'),
instructions: isDefault ? "The default shipping category is automatically available to all product types."|t('commerce') : "Which product types should this category be available to?"|t('commerce'),
warning: warning,
id: 'productTypes',
name: 'productTypes',
options: productTypesOptions,
values: shippingCategory is defined ? shippingCategory.productTypeIds : [],
values: productTypeValues,
showAllOption: false,
disabled: isDefault,
}) }}

{% if isDefault %}
{% for productTypeId in productTypesOptions|keys %}
{{ hiddenInput('productTypes[]', productTypeId) }}
{% endfor %}
{% endif %}

{% set defaultInput %}
{{ forms.lightswitchField({
instructions: "This category will be used as the default for all purchasables in this store."|t('commerce'),
Expand All @@ -80,3 +90,48 @@
new Craft.HandleGenerator('#{{ nameId }}', '#{{ handleId }}');
{% endjs %}
{% endif %}

{% set defaultSwitchId = 'default'|namespaceInputId|e('js') %}
{% set productTypesId = 'productTypes'|namespaceInputId|e('js') %}
{% js %}
(function() {
const defaultSwitch = document.getElementById('{{ defaultSwitchId }}');
const productTypesContainer = document.getElementById('{{ productTypesId }}');

if (!defaultSwitch || !productTypesContainer) return;

function updateProductTypes() {
const isDefault = defaultSwitch.classList.contains('on');
const checkboxes = productTypesContainer.querySelectorAll('input[type="checkbox"]');

checkboxes.forEach(checkbox => {
if (isDefault) {
checkbox.checked = true;
checkbox.disabled = true;
} else {
checkbox.disabled = false;
}
});

// Update hidden inputs
const existingHiddenInputs = productTypesContainer.parentElement.querySelectorAll('input[type="hidden"][name="productTypes[]"]');
existingHiddenInputs.forEach(input => input.remove());

if (isDefault) {
checkboxes.forEach(checkbox => {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'productTypes[]';
hiddenInput.value = checkbox.value;
productTypesContainer.parentElement.appendChild(hiddenInput);
});
}
}

// Listen for lightswitch changes
defaultSwitch.addEventListener('change', updateProductTypes);

// Also listen for the custom lightswitch event
$(defaultSwitch).on('change', updateProductTypes);
})();
{% endjs %}
3 changes: 2 additions & 1 deletion src/translations/en/commerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,8 @@
'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.' => 'The base discount can only discount items in the cart to down to zero until it is used up, it can not make the order negative.',
'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.' => 'The conversion rate that will be used when converting an amount to this currency. For example, if an item costs {amount1}, a conversion rate of {rate} would result in {amount2} in the alternate currency.',
'The countries that orders are allowed to be placed from.' => 'The countries that orders are allowed to be placed from.',
'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft’s General Settings.',
'The default shipping category is automatically available to all product types.' => 'The default shipping category is automatically available to all product types.',
'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft's General Settings.' => 'The email address that order status emails are sent from. Leave blank to use the System Email Address defined in Craft's General Settings.',

Check failure on line 1120 in src/translations/en/commerce.php

View workflow job for this annotation

GitHub Actions / ci / Code Quality / PHPStan / PHPStan

Syntax error, unexpected T_STRING, expecting ',' or ']' or ')' on line 1120
'The entry that contains the description for this subscription’s plan.' => 'The entry that contains the description for this subscription’s plan.',
'The flat value which should discount each item. i.e “3” for $3 off each item.' => 'The flat value which should discount each item. i.e “3” for $3 off each item.',
'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.' => 'The format used to generate new coupons, e.g. {example}. Any `#` characters will be replaced with a random letter.',
Expand Down
Loading