From 9f0e2be042ebe8bf59bcc63146262525185475ea Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 18 Nov 2024 16:53:30 +0000 Subject: [PATCH 01/44] Add coupon code condition rule and element query param --- CHANGELOG-WIP.md | 12 ++++- .../orders/CouponCodeConditionRule.php | 29 +++++++++++ .../conditions/orders/OrderCondition.php | 3 +- src/elements/db/OrderQuery.php | 52 +++++++++++++++++++ tests/unit/elements/order/OrderQueryTest.php | 39 ++++++++++++++ 5 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/elements/conditions/orders/CouponCodeConditionRule.php diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index de991b6ce5..ce8be4ab44 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,5 +1,13 @@ # Release Notes for Craft Commerce (WIP) -## Administration +### Administration +- Added a new "Coupon Code" order condition rule. ([#3776](https://github.com/craftcms/commerce/discussions/3776)) +- Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) -- Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) \ No newline at end of file +### Development +- Added the `couponCode` order query param. + +### Extensibility +- Added `craft\commerce\elements\conditions\orders\CouponCodeConditionRule`. +- Added `craft\commerce\elements\db\OrderQuery::$couponCode`. +- Added `craft\commerce\elements\db\OrderQuery::couponCode()`. \ No newline at end of file diff --git a/src/elements/conditions/orders/CouponCodeConditionRule.php b/src/elements/conditions/orders/CouponCodeConditionRule.php new file mode 100644 index 0000000000..e865619e48 --- /dev/null +++ b/src/elements/conditions/orders/CouponCodeConditionRule.php @@ -0,0 +1,29 @@ + + * @since 5.3.0 + */ +class CouponCodeConditionRule extends OrderTextValuesAttributeConditionRule +{ + public string $orderAttribute = 'couponCode'; + + /** + * @inheritdoc + */ + public function getLabel(): string + { + return Craft::t('commerce', 'Coupon Code'); + } +} diff --git a/src/elements/conditions/orders/OrderCondition.php b/src/elements/conditions/orders/OrderCondition.php index dd9e894c1c..15330cc8d0 100644 --- a/src/elements/conditions/orders/OrderCondition.php +++ b/src/elements/conditions/orders/OrderCondition.php @@ -30,8 +30,9 @@ protected function selectableConditionRules(): array { return array_merge(parent::selectableConditionRules(), [ DateOrderedConditionRule::class, - CustomerConditionRule::class, CompletedConditionRule::class, + CouponCodeConditionRule::class, + CustomerConditionRule::class, PaidConditionRule::class, HasPurchasableConditionRule::class, ItemSubtotalConditionRule::class, diff --git a/src/elements/db/OrderQuery.php b/src/elements/db/OrderQuery.php index a48d3b2626..cb1794e4a8 100644 --- a/src/elements/db/OrderQuery.php +++ b/src/elements/db/OrderQuery.php @@ -59,6 +59,12 @@ class OrderQuery extends ElementQuery */ public mixed $reference = null; + /** + * @var mixed The order reference of the resulting order. + * @used-by couponCode() + */ + public mixed $couponCode = null; + /** * @var mixed The email address the resulting orders must have. */ @@ -372,6 +378,48 @@ public function reference(mixed $value): OrderQuery return $this; } + /** + * Narrows the query results based on the order's coupon code. + * + * Possible values include: + * + * | Value | Fetches {elements}… + * | - | - + * | `':empty:'` | that don’t have a coupon code. + * | `':notempty:'` | that have a coupon code. + * | `'Foo'` | with a coupon code of `Foo`. + * | `'Foo*'` | with a coupon code that begins with `Foo`. + * | `'*Foo'` | with a coupon code that ends with `Foo`. + * | `'*Foo*'` | with a coupon code that contains `Foo`. + * | `'not *Foo*'` | with a coupon code that doesn’t contain `Foo`. + * | `['*Foo*', '*Bar*']` | with a coupon code that contains `Foo` or `Bar`. + * | `['not', '*Foo*', '*Bar*']` | with a coupon code that doesn’t contain `Foo` or `Bar`. + * + * --- + * + * ```twig + * {# Fetch the requested {element} #} + * {% set {element-var} = {twig-method} + * .reference('foo') + * .one() %} + * ``` + * + * ```php + * // Fetch the requested {element} + * ${element-var} = {php-method} + * ->reference('foo') + * ->one(); + * ``` + * + * @param string|null $value The property value + * @return static self reference + */ + public function couponCode(mixed $value): OrderQuery + { + $this->couponCode = $value; + return $this; + } + /** * Narrows the query results based on the customers’ email addresses. * @@ -1603,6 +1651,10 @@ protected function beforePrepare(): bool $this->subQuery->andWhere(Db::parseParam('commerce_orders.reference', $this->reference)); } + if (isset($this->couponCode)) { + $this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode)); + } + if (isset($this->email) && $this->email) { // Join and search the users table for email address $this->subQuery->leftJoin(CraftTable::USERS . ' users', '[[users.id]] = [[commerce_orders.customerId]]'); diff --git a/tests/unit/elements/order/OrderQueryTest.php b/tests/unit/elements/order/OrderQueryTest.php index 2c06a75e6c..9c6720864e 100644 --- a/tests/unit/elements/order/OrderQueryTest.php +++ b/tests/unit/elements/order/OrderQueryTest.php @@ -63,6 +63,45 @@ public function emailDataProvider(): array ]; } + /** + * @param string $couponCode + * @param int $count + * @return void + * @dataProvider couponCodeDataProvider + */ + public function testCouponCode(?string $couponCode, int $count): void + { + $ordersFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $ordersFixture->getElement('completed-new'); + + // Temporarily add a coupon code to an order + \craft\commerce\records\Order::updateAll(['couponCode' => 'foo'], ['id' => $order->id]); + + $orderQuery = Order::find(); + $orderQuery->couponCode($couponCode); + + self::assertCount($count, $orderQuery->all()); + + // Remove temporary coupon code + \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); + } + + /** + * @return array[] + */ + public function couponCodeDataProvider(): array + { + return [ + 'normal' => ['foo', 1], + 'case-insensitive' => ['fOo', 1], + 'using-null' => [null, 3], + 'empty-code' => [':empty:', 2], + 'not-empty-code' => [':notempty:', 1], + 'no-results' => ['nope', 0], + ]; + } + /** * @param mixed $handle * @param int $count From ebf639bd45569fe7b5e9a3ad2a259e193ac47efd Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 19 Nov 2024 09:05:00 +0000 Subject: [PATCH 02/44] Fix order query coupon code, should be case-insensitive --- src/elements/db/OrderQuery.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/elements/db/OrderQuery.php b/src/elements/db/OrderQuery.php index cb1794e4a8..18de59bf54 100644 --- a/src/elements/db/OrderQuery.php +++ b/src/elements/db/OrderQuery.php @@ -1652,7 +1652,8 @@ protected function beforePrepare(): bool } if (isset($this->couponCode)) { - $this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode)); + // Coupon code criteria is case-insensitive like in the adjuster + $this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode, caseInsensitive: true)); } if (isset($this->email) && $this->email) { From 664fc0f2fb6daf6566b66556da5dd0f7fec4c7fc Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 19 Nov 2024 14:18:42 +0000 Subject: [PATCH 03/44] `CouponCodeConditionRule` re-implements `matchValue()` method to be case insensitive --- .../orders/CouponCodeConditionRule.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/elements/conditions/orders/CouponCodeConditionRule.php b/src/elements/conditions/orders/CouponCodeConditionRule.php index e865619e48..a6c231899f 100644 --- a/src/elements/conditions/orders/CouponCodeConditionRule.php +++ b/src/elements/conditions/orders/CouponCodeConditionRule.php @@ -8,6 +8,8 @@ namespace craft\commerce\elements\conditions\orders; use Craft; +use craft\helpers\StringHelper; +use yii\base\InvalidConfigException; /** * Order Coupon Code condition rule. @@ -26,4 +28,30 @@ public function getLabel(): string { return Craft::t('commerce', 'Coupon Code'); } + + /** + * @inheritdoc + */ + protected function matchValue(mixed $value): bool + { + switch ($this->operator) { + case self::OPERATOR_EMPTY: + return !$value; + case self::OPERATOR_NOT_EMPTY: + return (bool)$value; + } + + if ($this->value === '') { + return true; + } + + return match ($this->operator) { + self::OPERATOR_EQ => strcasecmp($value, $this->value) === 0, + self::OPERATOR_NE => strcasecmp($value, $this->value) !== 0, + self::OPERATOR_BEGINS_WITH => is_string($value) && StringHelper::startsWith($value, $this->value, false), + self::OPERATOR_ENDS_WITH => is_string($value) && StringHelper::endsWith($value, $this->value, false), + self::OPERATOR_CONTAINS => is_string($value) && StringHelper::contains($value, $this->value, false), + default => throw new InvalidConfigException("Invalid operator: $this->operator"), + }; + } } From f3720592497ca56db38455ba609f18c00d3d9a76 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 19 Nov 2024 14:20:28 +0000 Subject: [PATCH 04/44] =?UTF-8?q?Make=20sure=20the=20discount=20order=20co?= =?UTF-8?q?ndition=20doesn=E2=80=99t=20have=20the=20coupon=20code=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/elements/conditions/orders/DiscountOrderCondition.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/elements/conditions/orders/DiscountOrderCondition.php b/src/elements/conditions/orders/DiscountOrderCondition.php index 14c7a730ad..4b35288076 100644 --- a/src/elements/conditions/orders/DiscountOrderCondition.php +++ b/src/elements/conditions/orders/DiscountOrderCondition.php @@ -5,6 +5,7 @@ use craft\commerce\base\HasStoreInterface; use craft\commerce\base\StoreTrait; use craft\elements\db\ElementQueryInterface; +use craft\helpers\ArrayHelper; use yii\base\NotSupportedException; /** @@ -41,7 +42,12 @@ protected function config(): array */ protected function selectableConditionRules(): array { - return array_merge(parent::selectableConditionRules(), []); + $rules = array_merge(parent::selectableConditionRules(), []); + + // We don't need the condition to have the coupon code rule + ArrayHelper::removeValue($rules, CouponCodeConditionRule::class); + + return $rules; } /** From 597f93a77f02143c72025795eae8a86b83ea92cc Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 19 Nov 2024 14:27:06 +0000 Subject: [PATCH 05/44] Add `CouponCodeConditionRule` to `OrderConditionTest` --- tests/unit/elements/order/conditions/OrderConditionTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/elements/order/conditions/OrderConditionTest.php b/tests/unit/elements/order/conditions/OrderConditionTest.php index b040a3cc97..fd5cdec51f 100644 --- a/tests/unit/elements/order/conditions/OrderConditionTest.php +++ b/tests/unit/elements/order/conditions/OrderConditionTest.php @@ -9,6 +9,7 @@ use Codeception\Test\Unit; use craft\commerce\elements\conditions\orders\CompletedConditionRule; +use craft\commerce\elements\conditions\orders\CouponCodeConditionRule; use craft\commerce\elements\conditions\orders\CustomerConditionRule; use craft\commerce\elements\conditions\orders\DateOrderedConditionRule; use craft\commerce\elements\conditions\orders\HasPurchasableConditionRule; @@ -66,8 +67,9 @@ public function testConditionRuleTypes(): void $rules = array_keys($rules); self::assertContains(DateOrderedConditionRule::class, $rules); - self::assertContains(CustomerConditionRule::class, $rules); self::assertContains(CompletedConditionRule::class, $rules); + self::assertContains(CouponCodeConditionRule::class, $rules); + self::assertContains(CustomerConditionRule::class, $rules); self::assertContains(PaidConditionRule::class, $rules); self::assertContains(HasPurchasableConditionRule::class, $rules); self::assertContains(ItemSubtotalConditionRule::class, $rules); From 609551c66d12cf0ce43eab16823fac95dbf8bfaa Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 19 Nov 2024 14:27:21 +0000 Subject: [PATCH 06/44] Add `CouponCodeConditionRuleTest` --- .../CouponCodeConditionRuleTest.php | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php diff --git a/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php b/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php new file mode 100644 index 0000000000..1905ef5bb6 --- /dev/null +++ b/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php @@ -0,0 +1,162 @@ + + * @since 5.3.0 + */ +class CouponCodeConditionRuleTest extends Unit +{ + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'orders' => [ + 'class' => OrdersFixture::class, + ], + ]; + } + + /** + * @group Order + * @dataProvider matchElementDataProvider + */ + public function testMatchElement(?string $coupon, string $operator = '=', ?string $orderCoupon = null, bool $expectedMatch = true): void + { + $condition = $this->_createCondition($coupon, $operator); + + $ordersFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $ordersFixture->getElement('completed-new'); + + if ($orderCoupon) { + $order->couponCode = $orderCoupon; + } + + $match = $condition->matchElement($order); + + if ($expectedMatch) { + self::assertTrue($match); + } else { + self::assertFalse($match); + } + } + + /** + * @return array[] + */ + public function matchElementDataProvider(): array + { + return [ + 'match-equals' => ['coupon1', '=', 'coupon1', true], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', true], + 'no-match-equals' => ['coupon1', '=', 'coupon2', false], + 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', false], + 'no-match-equals-null' => ['coupon1', '=', null, false], + 'match-contains' => ['coupon1', '**', 'coupon1', true], + 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', true], + 'no-match-contains' => ['coupon1', '**', 'coupon2', false], + 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', false], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', true], + 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', true], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', false], + 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', false], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', true], + 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', true], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', false], + 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', false], + ]; + } + + /** + * @group Order + * @dataProvider modifyQueryDataProvider + */ + public function testModifyQuery(?string $coupon, string $operator = '=', ?string $orderCoupon = null, int $expectedResults = 0): void + { + $condition = $this->_createCondition($coupon, $operator); + $orderFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $orderFixture->getElement('completed-new'); + + // Temporarily add a coupon code to an order + \craft\commerce\records\Order::updateAll(['couponCode' => $orderCoupon], ['id' => $order->id]); + + $query = Order::find(); + $condition->modifyQuery($query); + + self::assertCount($expectedResults, $query->ids()); + + if ($expectedResults > 0) { + self::assertContainsEquals($order->id, $query->ids()); + } else { + self::assertEmpty($query->ids()); + } + + // Remove temporary coupon code + \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); + } + + /** + * @return array[] + */ + public function modifyQueryDataProvider(): array + { + return [ + 'match-equals' => ['coupon1', '=', 'coupon1', 1], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', 1], + 'no-match-equals' => ['coupon1', '=', 'coupon2', 0], + 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', 0], + 'no-match-equals-null' => ['coupon1', '=', null, 0], + 'match-contains' => ['coupon1', '**', 'coupon1', 1], + 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', 1], + 'no-match-contains' => ['coupon1', '**', 'coupon2', 0], + 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', 0], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', 1], + 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', 1], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', 0], + 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', 0], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', 1], + 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', 1], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', 0], + 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', 0], + ]; + } + + /** + * @param string|null $value + * @param string|null $operator + * @return OrderCondition + */ + private function _createCondition(?string $value, ?string $operator = null): OrderCondition + { + $condition = Order::createCondition(); + /** @var CouponCodeConditionRule $rule */ + $rule = \Craft::$app->getConditions()->createConditionRule(CouponCodeConditionRule::class); + $rule->value = $value; + + if ($operator) { + $rule->operator = $operator; + } + + $condition->addConditionRule($rule); + + return $condition; + } +} From fd7140650baa5cb6922022cf1c527b7224daeb37 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 20 Nov 2024 23:01:29 +0800 Subject: [PATCH 07/44] Replaces #3772 --- src/elements/Product.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/elements/Product.php b/src/elements/Product.php index 20526d32c8..5a9478381a 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -1065,6 +1065,18 @@ public function getVariants(bool $includeDisabled = false): VariantCollection return $this->_variants->filter(fn(Variant $variant) => $includeDisabled || ($variant->getStatus() === self::STATUS_ENABLED)); } + /** + * @return VariantCollection + * @throws InvalidConfigException + * @internal Do not use. Temporary method until we get a nested element manager provider in core. + * + * TODO: Remove this once we have a nested element manager provider interface in core. + */ + public function getAllVariants(): VariantCollection + { + return $this->getVariants(true); + } + /** * @inheritdoc */ @@ -1187,9 +1199,9 @@ public function getVariantManager(): NestedElementManager /** @phpstan-ignore-next-line */ fn(Product $product) => self::createVariantQuery($product), [ - 'attribute' => 'variants', + 'attribute' => 'allVariants', // TODO: can change this back to 'variants' once we have a nested element manager provider in core. 'propagationMethod' => $this->getType()->propagationMethod, - 'valueGetter' => fn(Product $product) => $product->getVariants(true), + 'valueSetter' => fn($variants) => $this->setVariants($variants), // TODO: can change this back to 'variants' once we have a nested element manager provider in core. ], ); } From 09054df7748a609b2579bb51d370aeafa9432a0c Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 20 Nov 2024 23:02:50 +0800 Subject: [PATCH 08/44] Cleanup --- src/elements/Product.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/Product.php b/src/elements/Product.php index 5a9478381a..b8acf1a1e0 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -1069,7 +1069,7 @@ public function getVariants(bool $includeDisabled = false): VariantCollection * @return VariantCollection * @throws InvalidConfigException * @internal Do not use. Temporary method until we get a nested element manager provider in core. - * + * * TODO: Remove this once we have a nested element manager provider interface in core. */ public function getAllVariants(): VariantCollection From 714f74f18f66b2d01d422dad0f0f916ee3dfa9bc Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 21 Nov 2024 15:10:06 +0000 Subject: [PATCH 09/44] Bump Craft requirement --- composer.json | 2 +- composer.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index b3b4d39ca0..871b90968a 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "prefer-stable": true, "require": { "php": "^8.2", - "craftcms/cms": "^5.2.0", + "craftcms/cms": "^5.5.0", "dompdf/dompdf": "^2.0.2", "ibericode/vat": "^1.2.2", "iio/libmergepdf": "^4.0", diff --git a/composer.lock b/composer.lock index 44fc587fcd..a56e134c3b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f3ba664672e378629a5fd88f697019c", + "content-hash": "733b4b868ad8dbf52e6ea739a7a83494", "packages": [ { "name": "bacon/bacon-qr-code", @@ -331,16 +331,16 @@ }, { "name": "craftcms/cms", - "version": "5.5.0.1", + "version": "5.5.1.1", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "5e24a0bf74ea29ea2f5c04d4c9dcc325dcbb41ba" + "reference": "2233b27fd7e80cccc3aab927ad073f5916167dba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/5e24a0bf74ea29ea2f5c04d4c9dcc325dcbb41ba", - "reference": "5e24a0bf74ea29ea2f5c04d4c9dcc325dcbb41ba", + "url": "https://api.github.com/repos/craftcms/cms/zipball/2233b27fd7e80cccc3aab927ad073f5916167dba", + "reference": "2233b27fd7e80cccc3aab927ad073f5916167dba", "shasum": "" }, "require": { @@ -454,7 +454,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2024-11-13T14:36:24+00:00" + "time": "2024-11-19T02:11:31+00:00" }, { "name": "craftcms/plugin-installer", @@ -9423,16 +9423,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.10", + "version": "1.12.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", - "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "shasum": "" }, "require": { @@ -9477,7 +9477,7 @@ "type": "github" } ], - "time": "2024-11-11T15:37:09+00:00" + "time": "2024-11-17T14:08:01+00:00" }, { "name": "phpunit/php-code-coverage", From 242086009513fefcf0193aa6fb22534fb735f044 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 21 Nov 2024 15:10:29 +0000 Subject: [PATCH 10/44] WIP Card layout designer --- src/elements/Product.php | 28 +++++++++++++++++++ src/services/LineItems.php | 1 + .../settings/producttypes/_edit.twig | 2 ++ 3 files changed, 31 insertions(+) diff --git a/src/elements/Product.php b/src/elements/Product.php index 20526d32c8..ee853418b6 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -565,6 +565,34 @@ protected static function defineDefaultTableAttributes(string $source): array return $attributes; } + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(parent::defineCardAttributes(), [ + 'defaultPrice' => [ + 'label' => Craft::t('commerce', 'Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'defaultSku' => [ + 'label' => Craft::t('commerce', 'SKU'), + 'placeholder' => Html::tag('code', 'SKU123'), + ], + ]); + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'defaultSku', + 'defaultPrice', + ]); + } + /** * @inheritdoc */ diff --git a/src/services/LineItems.php b/src/services/LineItems.php index e7627fde24..812c1994cd 100644 --- a/src/services/LineItems.php +++ b/src/services/LineItems.php @@ -444,6 +444,7 @@ public function create(Order $order, array $params = [], LineItemType $type = Li } $params['class'] = LineItem::class; + /** @var LineItem $lineItem */ $lineItem = Craft::createObject($params); if ($lineItem->type === LineItemType::Purchasable) { diff --git a/src/templates/settings/producttypes/_edit.twig b/src/templates/settings/producttypes/_edit.twig index a290e42450..d607cb77d1 100644 --- a/src/templates/settings/producttypes/_edit.twig +++ b/src/templates/settings/producttypes/_edit.twig @@ -408,6 +408,7 @@ {{ forms.fieldLayoutDesignerField({ fieldLayout: productType.getProductFieldLayout(), + withCardViewDesigner: true, }) }} @@ -417,6 +418,7 @@ {% namespace "variant-layout" %} {{ forms.fieldLayoutDesignerField({ fieldLayout: productType.getVariantFieldLayout(), + withCardViewDesigner: true, }) }} {% endnamespace %} From 7ca145f376c732ce03981baf7f4c7dcafadb855a Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 21 Nov 2024 15:11:06 +0000 Subject: [PATCH 11/44] Changelog --- CHANGELOG-WIP.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index de991b6ce5..7608192b6f 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,5 +1,7 @@ # Release Notes for Craft Commerce (WIP) -## Administration +### Administration +- Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) -- Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) \ No newline at end of file +### System +- Craft Commerce now requires Craft CMS 5.5 or later. \ No newline at end of file From 92fc28fbd3b0350de7e34a11350bb96db1de089e Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 28 Nov 2024 17:21:05 +0000 Subject: [PATCH 12/44] WIP variant card attributes --- src/elements/Product.php | 11 ++++++ src/elements/Variant.php | 76 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/elements/Product.php b/src/elements/Product.php index ee853418b6..74dc8174e6 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -565,6 +565,17 @@ protected static function defineDefaultTableAttributes(string $source): array return $attributes; } + /** + * @inheritdoc + */ + public static function attributePreviewHtml(array $attribute): mixed + { + return match($attribute['value']) { + 'defaultSku' => $attribute['placeholder'], + default => parent::attributePreviewHtml($attribute) + }; + } + /** * @inheritdoc */ diff --git a/src/elements/Variant.php b/src/elements/Variant.php index f0ddc22b28..065f60015a 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -1329,6 +1329,73 @@ protected static function defineActions(string $source): array ]]; } + /** + * @inheritdoc + */ + public static function attributePreviewHtml(array $attribute): mixed + { + return match($attribute['value']) { + 'sku', 'priceView' => $attribute['placeholder'], + 'availableForPurchase', 'promotable' => Html::tag('span', '', [ + 'class' => 'checkbox-icon', + 'role' => 'img', + 'title' => $attribute['label'], + 'aria' => [ + 'label' => $attribute['label'], + ], + ]) . + Html::tag('span', $attribute['label'], [ + 'class' => 'checkbox-preview-label', + ]), + default => parent::attributePreviewHtml($attribute) + }; + } + + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(parent::defineCardAttributes(), [ + 'basePromotionalPrice' => [ + 'label' => Craft::t('commerce', 'Base Promotional Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'basePrice' => [ + 'label' => Craft::t('commerce', 'Base Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'product' => [ + 'label' => Craft::t('commerce', 'Product'), + ], + 'promotable' => [ + 'label' => Craft::t('commerce', 'Promotable'), + ], + 'availableForPurchase' => [ + 'label' => Craft::t('commerce', 'Available for purchase'), + ], + 'priceView' => [ + 'label' => Craft::t('commerce', 'Price'), + 'placeholder' => Html::tag('del','¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(199.99), ['style' => 'opacity: .5']) . ' ¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'sku' => [ + 'label' => Craft::t('commerce', 'SKU'), + 'placeholder' => Html::tag('code', 'SKU123'), + ], + ]); + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'sku', + 'priceView', + ]); + } + /** * @inheritdoc */ @@ -1377,6 +1444,15 @@ protected function attributeHtml(string $attribute): string return sprintf(' %s', $product->getStatus(), Html::encode($product->title)); } + if ($attribute === 'priceView') { + $price = $this->basePriceAsCurrency; + if ($this->getBasePromotionalPrice() && $this->getBasePromotionalPrice() < $this->getBasePrice()) { + $price = Html::tag('del', $price, ['style' => 'opacity: .5']) . ' ' . $this->basePromotionalPriceAsCurrency; + } + + return $price; + } + return parent::attributeHtml($attribute); } } From c0f47950c0ec49b16efe1594e24f8f747a908017 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 3 Dec 2024 10:17:58 +0000 Subject: [PATCH 13/44] #3792 Purchasbale line items missing their CP edit URL on order edit --- CHANGELOG.md | 4 ++++ src/elements/Variant.php | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c31dd94..364bd28bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes for Craft Commerce +## Unreleased + +- Fixed a bug where purchasable line items were missing their CP edit URL on the Edit Order page. ([#3792](https://github.com/craftcms/commerce/issues/3792)) + ## 5.2.7 - 2024-11 - Fixed an error that occurred on the Orders index page when running Craft CMS 5.5.4 or later. ([#3793](https://github.com/craftcms/commerce/issues/3793)) diff --git a/src/elements/Variant.php b/src/elements/Variant.php index f0ddc22b28..b052f0cf83 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -740,6 +740,14 @@ public function canView(User $user): bool return $product->canView($user); } + /** + * @inheritdoc + */ + public function getCpEditUrl(): ?string + { + return $this->getOwner() ? $this->getOwner()->getCpEditUrl() : ''; + } + /** * @inheritdoc */ From 4c12360d9cf84f6c5a29ff5935287a7ddfdc292d Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 4 Dec 2024 15:53:06 +0800 Subject: [PATCH 14/44] include locale value normalization Fixes $3789 --- CHANGELOG.md | 4 ++++ src/controllers/OrdersController.php | 4 ++++ .../_components/gateways/_modalWrapper.twig | 14 +++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c31dd94..b6d67c23cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes for Craft Commerce +## Unreleased + +- Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) + ## 5.2.7 - 2024-11 - Fixed an error that occurred on the Orders index page when running Craft CMS 5.5.4 or later. ([#3793](https://github.com/craftcms/commerce/issues/3793)) diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 87f871c06a..9b4d07f076 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -1236,11 +1236,15 @@ public function actionPaymentAmountData(): Response $paymentCurrencies = Plugin::getInstance()->getPaymentCurrencies(); $paymentCurrency = $this->request->getRequiredParam('paymentCurrency'); $paymentAmount = $this->request->getRequiredParam('paymentAmount'); + $locale = $this->request->getRequiredParam('locale'); $orderId = $this->request->getRequiredParam('orderId'); /** @var Order $order */ $order = Order::find()->id($orderId)->one(); $baseCurrency = $order->currency; + $paymentAmount = MoneyHelper::toMoney(['value' => $paymentAmount, 'currency' => $baseCurrency, 'locale' => $locale]); + $paymentAmount = MoneyHelper::toDecimal($paymentAmount); + $baseCurrencyPaymentAmount = $paymentCurrencies->convertCurrency($paymentAmount, $paymentCurrency, $baseCurrency); $baseCurrencyPaymentAmountAsCurrency = Craft::t('commerce', 'Pay {amount} of {currency} on the order.', ['amount' => Currency::formatAsCurrency($baseCurrencyPaymentAmount, $baseCurrency), 'currency' => $baseCurrency]); diff --git a/src/templates/_components/gateways/_modalWrapper.twig b/src/templates/_components/gateways/_modalWrapper.twig index 06ab826d5a..948ff011b4 100644 --- a/src/templates/_components/gateways/_modalWrapper.twig +++ b/src/templates/_components/gateways/_modalWrapper.twig @@ -20,9 +20,15 @@ {{ formHtml|raw }}
- Payment Amount + {{ "Payment Amount"|t('commerce') }}
- + + {% set currencies = craft.commerce.paymentCurrencies.getAllPaymentCurrencies() %} {% set primaryCurrency = craft.commerce.paymentCurrencies.getPrimaryPaymentCurrency() %} @@ -77,7 +83,8 @@ function updatePrice(form) { - var price = form.find("input.paymentAmount").val(); + var price = form.find("input[name='paymentAmount[value]']").val(); + var locale = form.find("input[name='paymentAmount[locale]']").val(); $.ajax({ type: "POST", @@ -89,6 +96,7 @@ data: { 'action' : 'commerce/orders/payment-amount-data', 'paymentAmount': price, + 'locale': locale, 'paymentCurrency': form.find(".paymentCurrency").val(), 'orderId' : orderId }, From eba27fffd0109ad517adc759e63d2f8c7d9413de Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 08:03:11 +0000 Subject: [PATCH 15/44] tidy --- src/elements/Variant.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/Variant.php b/src/elements/Variant.php index b052f0cf83..1ab9f48363 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -745,7 +745,7 @@ public function canView(User $user): bool */ public function getCpEditUrl(): ?string { - return $this->getOwner() ? $this->getOwner()->getCpEditUrl() : ''; + return $this->getOwner() ? $this->getOwner()->getCpEditUrl() : null; } /** From 6eb58c492b0b525063e619f4c359678566aef5a6 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 10:10:51 +0000 Subject: [PATCH 16/44] #3786 correctly save environment variables on store settings --- CHANGELOG.md | 1 + src/Plugin.php | 2 +- src/migrations/Install.php | 22 ++--- ...091901_fix_store_environment_variables.php | 97 +++++++++++++++++++ 4 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 src/migrations/m241204_091901_fix_store_environment_variables.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d67c23cc..12e9e9f024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) +- Fixed a bug where it wasn’t possible to set store settings to environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) ## 5.2.7 - 2024-11 diff --git a/src/Plugin.php b/src/Plugin.php index 8fef95aa8d..201e9b0bdb 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -257,7 +257,7 @@ public static function editions(): array /** * @inheritDoc */ - public string $schemaVersion = '5.2.7.0'; + public string $schemaVersion = '5.2.7.1'; /** * @inheritdoc diff --git a/src/migrations/Install.php b/src/migrations/Install.php index defc1bbf0c..756520a8ec 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -857,17 +857,17 @@ public function createTables(): void 'handle' => $this->string()->notNull(), 'primary' => $this->boolean()->notNull(), 'currency' => $this->string()->notNull()->defaultValue('USD'), - 'autoSetCartShippingMethodOption' => $this->boolean()->notNull()->defaultValue(false), - 'autoSetNewCartAddresses' => $this->boolean()->notNull()->defaultValue(false), - 'autoSetPaymentSource' => $this->boolean()->notNull()->defaultValue(false), - 'allowEmptyCartOnCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'allowCheckoutWithoutPayment' => $this->boolean()->notNull()->defaultValue(false), - 'allowPartialPaymentOnCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireShippingAddressAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireBillingAddressAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireShippingMethodSelectionAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'useBillingAddressForTax' => $this->boolean()->notNull()->defaultValue(false), - 'validateOrganizationTaxIdAsVatId' => $this->boolean()->notNull()->defaultValue(false), + 'autoSetCartShippingMethodOption' => $this->string()->notNull()->defaultValue('false'), + 'autoSetNewCartAddresses' => $this->string()->notNull()->defaultValue('false'), + 'autoSetPaymentSource' => $this->string()->notNull()->defaultValue('false'), + 'allowEmptyCartOnCheckout' => $this->string()->notNull()->defaultValue('false'), + 'allowCheckoutWithoutPayment' => $this->string()->notNull()->defaultValue('false'), + 'allowPartialPaymentOnCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireShippingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireBillingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireShippingMethodSelectionAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'useBillingAddressForTax' => $this->string()->notNull()->defaultValue('false'), + 'validateOrganizationTaxIdAsVatId' => $this->string()->notNull()->defaultValue('false'), 'orderReferenceFormat' => $this->string(), 'freeOrderPaymentStrategy' => $this->string()->defaultValue('complete'), 'minimumTotalPriceStrategy' => $this->string()->defaultValue('default'), diff --git a/src/migrations/m241204_091901_fix_store_environment_variables.php b/src/migrations/m241204_091901_fix_store_environment_variables.php new file mode 100644 index 0000000000..4b11171509 --- /dev/null +++ b/src/migrations/m241204_091901_fix_store_environment_variables.php @@ -0,0 +1,97 @@ +from('{{%commerce_stores}}') + ->all(); + + // Get the store settings for each store from the project config + $storeSettings = \Craft::$app->getProjectConfig()->get('commerce.stores'); + + + // Store properties to update + $storeProperties = [ + 'autoSetNewCartAddresses', + 'autoSetCartShippingMethodOption', + 'autoSetPaymentSource', + 'allowEmptyCartOnCheckout', + 'allowCheckoutWithoutPayment', + 'allowPartialPaymentOnCheckout', + 'requireShippingAddressAtCheckout', + 'requireBillingAddressAtCheckout', + 'requireShippingMethodSelectionAtCheckout', + 'useBillingAddressForTax', + 'validateOrganizationTaxIdAsVatId', + ]; + + // Update stores env var DB columns + foreach ($storeProperties as $storeProperty) { + $this->alterColumn('{{%commerce_stores}}', $storeProperty, $this->string()->notNull()->defaultValue('false')); + } + + // Loop through each store and update values in the DB to match the PC values + foreach ($stores as $store) { + $storeSettingsForStore = $storeSettings[$store['uid']] ?? null; + + // If there isn't data in the PC for this store, skip it + if (!$storeSettingsForStore) { + continue; + } + + $updateData = []; + foreach ($storeProperties as $storeProperty) { + // If there isn't data in the PC for this store property, skip it + if (!isset($storeSettingsForStore[$storeProperty])) { + continue; + } + + // If the value in PC is a bool and the same as the DB value, skip it + if (in_array($storeSettingsForStore[$storeProperty], ['0', '1', 0, 1, false, true, 'false', 'true'], true) && $storeSettingsForStore[$storeProperty] == $store[$storeProperty]) { + continue; + } + + // If the value in PC is a string and is different from the DB value, skip it to avoid change in behavior + $envVarValue = App::parseBooleanEnv($storeSettingsForStore[$storeProperty]); + if ($envVarValue != $store[$storeProperty]) { + continue; + } + + // Else update the DB with the environment variable name + $updateData[$storeProperty] = $storeSettingsForStore[$storeProperty]; + } + + if (empty($updateData)) { + continue; + } + + $this->update('{{%commerce_stores}}', $updateData, ['id' => $store['id']]); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m241204_091901_fix_store_environment_variables cannot be reverted.\n"; + return false; + } +} From 251b19ae8329f04111c0ae5ce8b2945e996286a8 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 10:13:02 +0000 Subject: [PATCH 17/44] Use table constants --- .../m241204_091901_fix_store_environment_variables.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/migrations/m241204_091901_fix_store_environment_variables.php b/src/migrations/m241204_091901_fix_store_environment_variables.php index 4b11171509..ea0d504579 100644 --- a/src/migrations/m241204_091901_fix_store_environment_variables.php +++ b/src/migrations/m241204_091901_fix_store_environment_variables.php @@ -2,6 +2,7 @@ namespace craft\commerce\migrations; +use craft\commerce\db\Table; use craft\db\Migration; use craft\db\Query; use craft\helpers\App; @@ -18,7 +19,7 @@ public function safeUp(): bool { // Get all the stores current data $stores = (new Query()) - ->from('{{%commerce_stores}}') + ->from(Table::STORES) ->all(); // Get the store settings for each store from the project config @@ -42,7 +43,7 @@ public function safeUp(): bool // Update stores env var DB columns foreach ($storeProperties as $storeProperty) { - $this->alterColumn('{{%commerce_stores}}', $storeProperty, $this->string()->notNull()->defaultValue('false')); + $this->alterColumn(Table::STORES, $storeProperty, $this->string()->notNull()->defaultValue('false')); } // Loop through each store and update values in the DB to match the PC values @@ -80,7 +81,7 @@ public function safeUp(): bool continue; } - $this->update('{{%commerce_stores}}', $updateData, ['id' => $store['id']]); + $this->update(Table::STORES, $updateData, ['id' => $store['id']]); } return true; From 8ed09c9c2e5820cdcb719656cb3cb85f73214cde Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 10:17:44 +0000 Subject: [PATCH 18/44] PHPStan fix --- src/controllers/OrdersController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 9b4d07f076..fbeac219fa 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -1245,7 +1245,7 @@ public function actionPaymentAmountData(): Response $paymentAmount = MoneyHelper::toMoney(['value' => $paymentAmount, 'currency' => $baseCurrency, 'locale' => $locale]); $paymentAmount = MoneyHelper::toDecimal($paymentAmount); - $baseCurrencyPaymentAmount = $paymentCurrencies->convertCurrency($paymentAmount, $paymentCurrency, $baseCurrency); + $baseCurrencyPaymentAmount = $paymentCurrencies->convertCurrency((float)$paymentAmount, $paymentCurrency, $baseCurrency); $baseCurrencyPaymentAmountAsCurrency = Craft::t('commerce', 'Pay {amount} of {currency} on the order.', ['amount' => Currency::formatAsCurrency($baseCurrencyPaymentAmount, $baseCurrency), 'currency' => $baseCurrency]); $outstandingBalance = $order->outstandingBalance; From 308c1e126a8a5c9fc9018fc7e8264ccf40006c6e Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 4 Dec 2024 18:51:03 +0800 Subject: [PATCH 19/44] Fixed Drafts showing up in inventory --- CHANGELOG.md | 1 + src/controllers/InventoryController.php | 2 ++ src/services/Inventory.php | 6 +++++- src/services/InventoryLocations.php | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6d67c23cc..e35f416765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Fixed a bug where draft purchasables would show up on the Inventory page. - Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) ## 5.2.7 - 2024-11 diff --git a/src/controllers/InventoryController.php b/src/controllers/InventoryController.php index 13e2653aea..7d083825f7 100644 --- a/src/controllers/InventoryController.php +++ b/src/controllers/InventoryController.php @@ -242,6 +242,8 @@ public function actionInventoryLevelsTableData(): Response $inventoryQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[ii.purchasableId]] = [[purchasables.id]]'); $inventoryQuery->addGroupBy(['[[purchasables.description]]', '[[purchasables.sku]]']); + $inventoryQuery->andWhere(['not', ['elements.id' => null]]); + if ($search) { $inventoryQuery->andWhere(['or', ['like', 'purchasables.description', $search], ['like', 'purchasables.sku', $search]]); } diff --git a/src/services/Inventory.php b/src/services/Inventory.php index 93107c4fba..01ecd7a4be 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -262,8 +262,12 @@ public function getInventoryLevelQuery(?int $limit = null, ?int $offset = null, ->limit($limit) ->offset($offset); + $query->leftJoin( + ['elements' => CraftTable::ELEMENTS], + '[[ii.purchasableId]] = [[elements.id]] AND [[elements.draftId]] IS NULL AND [[elements.revisionId]] IS NULL' + ); + if (!$withTrashed) { - $query->leftJoin(['elements' => CraftTable::ELEMENTS], '[[ii.purchasableId]] = [[elements.id]]'); $query->andWhere(['elements.dateDeleted' => null]); } diff --git a/src/services/InventoryLocations.php b/src/services/InventoryLocations.php index c92e806797..31ead4e14f 100644 --- a/src/services/InventoryLocations.php +++ b/src/services/InventoryLocations.php @@ -283,6 +283,7 @@ private function _createInventoryLocationsQuery(bool $withTrashed = false): Quer 'dateCreated', 'dateUpdated', ]) + ->orderBy(['name' => SORT_ASC]) ->from([Table::INVENTORYLOCATIONS]); if (!$withTrashed) { From 2476f8174af13a822db72e3dd0030c5d6637f8c6 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 4 Dec 2024 20:16:45 +0800 Subject: [PATCH 20/44] Dont get drafts --- src/services/Inventory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/Inventory.php b/src/services/Inventory.php index 01ecd7a4be..0ffa7726d6 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -207,6 +207,7 @@ public function getInventoryLocationLevels(InventoryLocation $inventoryLocation, { $levels = $this->getInventoryLevelQuery(withTrashed: $withTrashed) ->andWhere(['inventoryLocationId' => $inventoryLocation->id]) + ->andWhere(['not', ['elements.id' => null]]) ->collect(); $inventoryItems = Plugin::getInstance()->getInventory()->getInventoryItemsByIds($levels->pluck('inventoryItemId')->unique()->toArray()); From 0c70bfaea1c2db4f7f74a0881e4212ef45958792 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 4 Dec 2024 20:18:16 +0800 Subject: [PATCH 21/44] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35f416765..bef1b09cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Fixed a bug where draft purchasables would show up on the Inventory page. +- Fixed a PHP error that could occur when creating inventory transfers. - Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) ## 5.2.7 - 2024-11 From cd2a61ab484944af1c318b0eee49472ef5d53e31 Mon Sep 17 00:00:00 2001 From: Luke Holder Date: Wed, 4 Dec 2024 20:40:29 +0800 Subject: [PATCH 22/44] Fixed type error --- CHANGELOG.md | 2 +- src/collections/UpdateInventoryLevelCollection.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bef1b09cdc..c28b0bc97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Fixed a bug where draft purchasables would show up on the Inventory page. -- Fixed a PHP error that could occur when creating inventory transfers. +- Fixed a PHP error that could occur when creating inventory transfers. ([#3696](https://github.com/craftcms/commerce/issues/3696)) - Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) ## 5.2.7 - 2024-11 diff --git a/src/collections/UpdateInventoryLevelCollection.php b/src/collections/UpdateInventoryLevelCollection.php index afbfe84f22..8c57c200a8 100644 --- a/src/collections/UpdateInventoryLevelCollection.php +++ b/src/collections/UpdateInventoryLevelCollection.php @@ -8,6 +8,7 @@ namespace craft\commerce\collections; use craft\commerce\models\inventory\UpdateInventoryLevel; +use craft\commerce\models\inventory\UpdateInventoryLevelInTransfer; use Illuminate\Support\Collection; /** @@ -46,7 +47,7 @@ public static function make($items = []) */ public function getPurchasables(): array { - return $this->map(function(UpdateInventoryLevel $updateInventoryLevel) { + return $this->map(function(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) { return $updateInventoryLevel->inventoryItem->getPurchasable(); })->all(); } From 0cf2242f74f9c35f08ebcce9e2a84a8e161a21d3 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 14:21:03 +0000 Subject: [PATCH 23/44] Match DB values to what is stored in PC --- .../m241204_091901_fix_store_environment_variables.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/migrations/m241204_091901_fix_store_environment_variables.php b/src/migrations/m241204_091901_fix_store_environment_variables.php index ea0d504579..279ac79e65 100644 --- a/src/migrations/m241204_091901_fix_store_environment_variables.php +++ b/src/migrations/m241204_091901_fix_store_environment_variables.php @@ -62,18 +62,12 @@ public function safeUp(): bool continue; } - // If the value in PC is a bool and the same as the DB value, skip it - if (in_array($storeSettingsForStore[$storeProperty], ['0', '1', 0, 1, false, true, 'false', 'true'], true) && $storeSettingsForStore[$storeProperty] == $store[$storeProperty]) { - continue; - } - - // If the value in PC is a string and is different from the DB value, skip it to avoid change in behavior + // Parse the value from the PC $envVarValue = App::parseBooleanEnv($storeSettingsForStore[$storeProperty]); - if ($envVarValue != $store[$storeProperty]) { + if ($envVarValue === null) { continue; } - // Else update the DB with the environment variable name $updateData[$storeProperty] = $storeSettingsForStore[$storeProperty]; } From 48e3dcb2915475cb62773b9b9742363c879aea50 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 14:25:44 +0000 Subject: [PATCH 24/44] Update changelog [ci skip] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3521bde6b2..c08e5a01ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Fixed a bug where draft purchasables would show up on the Inventory page. - Fixed a PHP error that could occur when creating inventory transfers. ([#3696](https://github.com/craftcms/commerce/issues/3696)) - Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) -- Fixed a bug where it wasn’t possible to set store settings to environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) +- Fixed a bug where it store settings weren’t respecting environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) ## 5.2.7 - 2024-11 From 8b0cc543284adb660c9e6bd232dcf268b013725c Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 4 Dec 2024 16:15:03 +0000 Subject: [PATCH 25/44] Fixed typecasting `isLite` causing deprecation log --- src/models/ShippingMethod.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/ShippingMethod.php b/src/models/ShippingMethod.php index 79fc4edd50..b9f951390c 100644 --- a/src/models/ShippingMethod.php +++ b/src/models/ShippingMethod.php @@ -38,7 +38,6 @@ public function behaviors(): array 'name' => AttributeTypecastBehavior::TYPE_STRING, 'handle' => AttributeTypecastBehavior::TYPE_STRING, 'enabled' => AttributeTypecastBehavior::TYPE_BOOLEAN, - 'isLite' => AttributeTypecastBehavior::TYPE_BOOLEAN, ], ]; From 4f57dc1f3691bed9900ce4b198ab7e918e9a1f08 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 4 Dec 2024 10:47:43 -0800 Subject: [PATCH 26/44] Changelog tweaks --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08e5a01ee..a764a4d16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,13 @@ ## Unreleased -- Fixed a bug where purchasable line items were missing their CP edit URL on the Edit Order page. ([#3792](https://github.com/craftcms/commerce/issues/3792)) -- Fixed a bug where draft purchasables would show up on the Inventory page. +- Fixed a bug where line items weren’t getting hyperlinked within Edit Order pages. ([#3792](https://github.com/craftcms/commerce/issues/3792)) +- Fixed a bug where Inventory pages were showing draft purchasables. - Fixed a PHP error that could occur when creating inventory transfers. ([#3696](https://github.com/craftcms/commerce/issues/3696)) -- Fixed a bug where the price was not formatted correctly according to the locale in the payment model on the Order Edit screens. ([#3789](https://github.com/craftcms/commerce/issues/3789)) -- Fixed a bug where it store settings weren’t respecting environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) +- Fixed a bug where prices weren’t getting formatted per the user’s formatting locale, in payment models on Edit Order pages. ([#3789](https://github.com/craftcms/commerce/issues/3789)) +- Fixed a bug where store settings weren’t respecting environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) -## 5.2.7 - 2024-11 +## 5.2.7 - 2024-12-02 - Fixed an error that occurred on the Orders index page when running Craft CMS 5.5.4 or later. ([#3793](https://github.com/craftcms/commerce/issues/3793)) - Fixed a bug where a structured product type’s “Max Levels” setting wasn’t being respected. ([#3785](https://github.com/craftcms/commerce/issues/3785)) From bf0c75fe823d556bc08e58bd9e174c5bcc17b374 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 4 Dec 2024 10:48:07 -0800 Subject: [PATCH 27/44] Finish 5.2.8 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a764a4d16b..a30de6c0dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Notes for Craft Commerce -## Unreleased +## 5.2.8 - 2024-12-04 - Fixed a bug where line items weren’t getting hyperlinked within Edit Order pages. ([#3792](https://github.com/craftcms/commerce/issues/3792)) - Fixed a bug where Inventory pages were showing draft purchasables. From 5daeac9d8dc6a698c22e8397882cf07d22c93b62 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 11:03:25 +0000 Subject: [PATCH 28/44] WIP inventory item steamline --- src/base/InventoryItemTrait.php | 61 ++++++++++++++ src/base/InventoryLocationTrait.php | 61 ++++++++++++++ src/base/InventoryMovement.php | 23 ++--- src/base/InventoryMovementInterface.php | 2 +- src/base/Purchasable.php | 64 ++++++++++++++ src/base/StoreTrait.php | 5 ++ .../UpdateInventoryLevelCollection.php | 5 +- src/controllers/InventoryController.php | 77 +++++++---------- src/controllers/OrdersController.php | 4 +- src/controllers/TransfersController.php | 6 +- src/elements/Transfer.php | 36 ++++---- .../inventory/InventoryManualMovement.php | 4 +- .../inventory/InventoryTransferMovement.php | 8 -- src/models/inventory/UpdateInventoryLevel.php | 15 +--- .../UpdateInventoryLevelInTransfer.php | 14 +--- src/services/Inventory.php | 83 ++++++++++--------- src/services/InventoryLocations.php | 2 +- 17 files changed, 319 insertions(+), 151 deletions(-) create mode 100644 src/base/InventoryItemTrait.php create mode 100644 src/base/InventoryLocationTrait.php diff --git a/src/base/InventoryItemTrait.php b/src/base/InventoryItemTrait.php new file mode 100644 index 0000000000..1bfc485d20 --- /dev/null +++ b/src/base/InventoryItemTrait.php @@ -0,0 +1,61 @@ + + * @since 5.3.0 + */ +trait InventoryItemTrait +{ + /** + * @var int|null The inventory item ID + */ + public ?int $inventoryItemId = null; + + /** + * @var InventoryItem|null The inventory item + * @see getInventoryItem() + * @see setInventoryItem() + */ + private ?InventoryItem $_inventoryItem = null; + + /** + * @param InventoryItem|null $inventoryItem + * @return void + */ + public function setInventoryItem(?InventoryItem $inventoryItem): void + { + $this->_inventoryItem = $inventoryItem; + $this->inventoryItemId = $inventoryItem?->id ?? null; + } + + /** + * @return InventoryItem|null + * @throws \yii\base\InvalidConfigException + */ + public function getInventoryItem(): ?InventoryItem + { + if (isset($this->_inventoryItem)) { + return $this->_inventoryItem; + } + + if ($this->inventoryItemId) { + $this->_inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId); + + return $this->_inventoryItem; + } + + return null; + } +} diff --git a/src/base/InventoryLocationTrait.php b/src/base/InventoryLocationTrait.php new file mode 100644 index 0000000000..02888bc927 --- /dev/null +++ b/src/base/InventoryLocationTrait.php @@ -0,0 +1,61 @@ + + * @since 5.3.0 + */ +trait InventoryLocationTrait +{ + /** + * @var int|null The inventory item ID + */ + public ?int $inventoryLocationId = null; + + /** + * @var InventoryLocation|null The inventory item + * @see getInventoryLocation() + * @see setInventoryLocation() + */ + private ?InventoryLocation $_inventoryLocation = null; + + /** + * @param InventoryLocation|null $inventoryLocation + * @return void + */ + public function setInventoryLocation(?InventoryLocation $inventoryLocation): void + { + $this->_inventoryLocation = $inventoryLocation; + $this->inventoryLocationId = $inventoryLocation?->id ?? null; + } + + /** + * @return InventoryLocation|null + * @throws \yii\base\InvalidConfigException + */ + public function getInventoryLocation(): ?InventoryLocation + { + if (isset($this->_inventoryLocation)) { + return $this->_inventoryLocation; + } + + if ($this->inventoryLocationId) { + $this->_inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId); + + return $this->_inventoryLocation; + } + + return null; + } +} diff --git a/src/base/InventoryMovement.php b/src/base/InventoryMovement.php index 6675a1ff48..f858d301a1 100644 --- a/src/base/InventoryMovement.php +++ b/src/base/InventoryMovement.php @@ -18,10 +18,7 @@ */ abstract class InventoryMovement extends Model implements InventoryMovementInterface { - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; + use InventoryItemTrait; /** * @var InventoryLocation @@ -90,27 +87,31 @@ public function init(): void } /** - * @inheritDoc + * @return array */ - public function isValid(): bool + protected function defineRules(): array { - return $this->validate(); + $rules = parent::defineRules(); + + $rules[] = [['inventoryItemId'], 'safe']; + + return $rules; } /** * @inheritDoc */ - public function getInventoryMovementHash(): string + public function isValid(): bool { - return $this->_inventoryMovementHash; + return $this->validate(); } /** * @inheritDoc */ - public function getInventoryItem(): InventoryItem + public function getInventoryMovementHash(): string { - return $this->inventoryItem; + return $this->_inventoryMovementHash; } /** diff --git a/src/base/InventoryMovementInterface.php b/src/base/InventoryMovementInterface.php index 2ea87f0c0a..7e29090bad 100644 --- a/src/base/InventoryMovementInterface.php +++ b/src/base/InventoryMovementInterface.php @@ -21,7 +21,7 @@ interface InventoryMovementInterface /** * @return InventoryItem */ - public function getInventoryItem(): InventoryItem; + public function getInventoryItem(): ?InventoryItem; /** * @return InventoryLocation diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index ce7906982c..1701e4981e 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -10,12 +10,14 @@ use Craft; use craft\base\Element; use craft\base\NestedElementInterface; +use craft\commerce\collections\UpdateInventoryLevelCollection; use craft\commerce\db\Table; use craft\commerce\elements\Order; use craft\commerce\errors\StoreNotFoundException; use craft\commerce\helpers\Currency; use craft\commerce\helpers\Localization; use craft\commerce\helpers\Purchasable as PurchasableHelper; +use craft\commerce\models\inventory\UpdateInventoryLevel; use craft\commerce\models\InventoryItem; use craft\commerce\models\InventoryLevel; use craft\commerce\models\LineItem; @@ -949,6 +951,68 @@ public function getStock(): int return $this->_stock; } + /** + * @return void + * @since 5.3.0 + */ + public function setStockLevel($amount): void + { + + // $commercePlugin = Plugin::getInstance(); + // + // // For simplicity let's just get the first inventory location + // $inventoryLocation = $commercePlugin->getInventoryLocations()->getAllInventoryLocations()->first(); + // + // // Retrieve the inventory item ID for the variants you want to update + // $variantInventoryItemQuery = \craft\commerce\elements\Variant::find() + // // ->select(['inventoryitems.id as inventoryItemId']); + // ->select(['inventoryitems.id as inventoryItemId']) + // ->andWhere(['elements.id' => 327]); + // // Can add extra criteria to the query if needed + // + // // Retrieve the inventory items for the variants + // $inventoryItems = $commercePlugin->getInventory()->getInventoryItemQuery() + // ->andWhere(['id' => $variantInventoryItemQuery]) + // ->collect() + // ->map(function($inventoryItem) { + // return new InventoryItem($inventoryItem); + // }); + // + // // Retrieve the inventory levels for the inventory items + // $inventoryLevels = $commercePlugin->getInventory()->getInventoryLevelQuery() + // ->andWhere(['ii.id' => $variantInventoryItemQuery]) + // ->andWhere(['it.inventoryLocationId' => $inventoryLocation->id]) + // ->collect() + // ->map(function($inventoryLevel) { + // unset($inventoryLevel['purchasableId']); + // return new InventoryLevel($inventoryLevel); + // }); + // + // $updateInventoryLevels = UpdateInventoryLevelCollection::make(); + // + // $inventoryLevels->each(function(InventoryLevel $inventoryLevel) use ($updateInventoryLevels, $inventoryItems, $inventoryLocation) { + // // Update action can be `SET` or `ADJUST` based on inserting or updating the stock + // $updateAction = \craft\commerce\enums\InventoryUpdateQuantityType::SET; + // + // // Stock amount, this would need to be based on your requirements/data being imported + // $stock = 1; // stock amount + // + // $updateInventoryLevels->push(new UpdateInventoryLevel([ + // 'type' => InventoryTransactionType::AVAILABLE->value, + // 'updateAction' => $updateAction, + // 'inventoryItem' => $inventoryItems->firstWhere('id', $inventoryLevel->inventoryItemId), + // 'inventoryLocation' => $inventoryLocation, + // 'quantity' => $stock, + // 'note' => '', + // ]) + // ); + // }); + // + // if ($updateInventoryLevels->count() > 0) { + // $commercePlugin->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); + // } + } + /** * Returns the total stock across all locations this purchasable is tracked in. diff --git a/src/base/StoreTrait.php b/src/base/StoreTrait.php index 8cec683c28..cc92f91216 100644 --- a/src/base/StoreTrait.php +++ b/src/base/StoreTrait.php @@ -1,4 +1,9 @@ map(function(UpdateInventoryLevel $updateInventoryLevel) { - return $updateInventoryLevel->inventoryItem->getPurchasable(); + return $this->map(function(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) { + return $updateInventoryLevel->getInventoryItem()->getPurchasable(); })->all(); } } diff --git a/src/controllers/InventoryController.php b/src/controllers/InventoryController.php index 7d083825f7..8ebfb7d3af 100644 --- a/src/controllers/InventoryController.php +++ b/src/controllers/InventoryController.php @@ -503,7 +503,6 @@ public function actionUpdateLevels(): Response $note = Craft::$app->getRequest()->getRequiredParam('note'); $inventoryLocationId = (int)Craft::$app->getRequest()->getRequiredParam('inventoryLocationId'); $inventoryItemIds = Craft::$app->getRequest()->getRequiredParam('ids'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); $type = Craft::$app->getRequest()->getRequiredParam('type'); // We don't add zero amounts as transactions movements @@ -514,17 +513,16 @@ public function actionUpdateLevels(): Response $errors = []; $updateInventoryLevels = UpdateInventoryLevelCollection::make(); foreach ($inventoryItemIds as $inventoryItemId) { - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId); - - $updateInventoryLevels->push(new UpdateInventoryLevel([ - 'type' => $type, - 'updateAction' => $updateAction, - 'inventoryItem' => $inventoryItem, - 'inventoryLocation' => $inventoryLocation, - 'quantity' => $quantity, - 'note' => $note, - ]) - ); + // Verbosely set property to show usages + $updateInventoryLevel = new UpdateInventoryLevel(); + $updateInventoryLevel->type = $type; + $updateInventoryLevel->updateAction = $updateAction; + $updateInventoryLevel->inventoryItemId = $inventoryItemId; + $updateInventoryLevel->inventoryLocationId = $inventoryLocationId; + $updateInventoryLevel->quantity = $quantity; + $updateInventoryLevel->note = $note; + + $updateInventoryLevels->push($updateInventoryLevel); } @@ -540,7 +538,8 @@ public function actionUpdateLevels(): Response $resultingInventoryLevels = []; foreach ($updateInventoryLevels as $updateInventoryLevel) { - $resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItem, $updateInventoryLevel->inventoryLocation); + /** @var UpdateInventoryLevel $updateInventoryLevel */ + $resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItemId, $updateInventoryLevel->inventoryLocationId); } return $this->asSuccess(Craft::t('commerce', 'Inventory updated.'), [ @@ -565,12 +564,9 @@ public function actionEditUpdateLevelsModal(): Response $quantity = (int)$this->request->getParam('quantity', 0); $type = $this->request->getRequiredParam('type'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); - $inventoryLevels = []; foreach ($inventoryItemIds as $inventoryItemId) { - $item = Plugin::getInstance()->getInventory()->getInventoryItemById((int)$inventoryItemId); - $inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($item, $inventoryLocation); + $inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel((int)$inventoryItemId, $inventoryLocationId); } $params = [ @@ -614,17 +610,14 @@ public function actionSaveInventoryMovement(): Response return $this->asSuccess(Craft::t('commerce', 'No inventory movements made.')); } - $inventoryMovement = new InventoryManualMovement( - [ - 'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId), - 'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId), - 'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId), - 'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType), - 'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType), - 'quantity' => $quantity, - 'note' => $note, - ] - ); + $inventoryMovement = new InventoryManualMovement(); + $inventoryMovement->inventoryItemId = $inventoryItemId; + $inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId); + $inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId); + $inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType); + $inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType); + $inventoryMovement->quantity = $quantity; + $inventoryMovement->note = $note; if ($inventoryMovement->validate()) { /** @var InventoryMovementCollection $inventoryMovementCollection */ @@ -665,19 +658,16 @@ public function actionEditMovementModal(): Response $toInventoryTransactionType = $toInventoryTransactionType->value; } - $inventoryMovement = new InventoryManualMovement( - [ - 'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId), - 'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId), - 'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId), - 'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType), - 'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType), - 'quantity' => $quantity, - 'note' => $note, - ] - ); - - $fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItem, $inventoryMovement->fromInventoryLocation); + $inventoryMovement = new InventoryManualMovement(); + $inventoryMovement->inventoryItemId = $inventoryItemId; + $inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId); + $inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId); + $inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType); + $inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType); + $inventoryMovement->quantity = $quantity; + $inventoryMovement->note = $note; + + $fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItemId, $inventoryMovement->fromInventoryLocation); $fromTotal = $fromLevel->{$fromInventoryTransactionType . 'Total'}; $movableTo = $movableTo->toArray(); @@ -705,10 +695,7 @@ public function actionUnfulfilledOrders(): Response $inventoryLocationId = Craft::$app->getRequest()->getParam('inventoryLocationId'); $inventoryItemId = Craft::$app->getRequest()->getParam('inventoryItemId'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId); - - $orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItem, $inventoryLocation); + $orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItemId, $inventoryLocationId); $title = Craft::t('commerce', '{count} Unfulfilled Orders', [ 'count' => count($orders), diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 390a0b9b6a..5da4a7fc34 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -259,10 +259,10 @@ public function actionFulfill(): Response $qty = (int)$fulfillment['quantity']; if ($qty != 0) { $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fulfillment['inventoryLocationId']); - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($fulfillment['inventoryItemId']); + $movement = new InventoryFulfillMovement(); $movement->fromInventoryLocation = $inventoryLocation; - $movement->inventoryItem = $inventoryItem; + $movement->inventoryItemId = $fulfillment['inventoryItemId']; $movement->toInventoryLocation = $inventoryLocation; $movement->fromInventoryTransactionType = InventoryTransactionType::COMMITTED; $movement->toInventoryTransactionType = InventoryTransactionType::FULFILLED; diff --git a/src/controllers/TransfersController.php b/src/controllers/TransfersController.php index a6fcf304ba..0c278fdb69 100644 --- a/src/controllers/TransfersController.php +++ b/src/controllers/TransfersController.php @@ -197,7 +197,7 @@ public function actionReceiveTransfer(): Response $inventoryAcceptedMovement = new InventoryTransferMovement(); $inventoryAcceptedMovement->quantity = $acceptedAmount; $inventoryAcceptedMovement->transferId = $transfer->id; - $inventoryAcceptedMovement->inventoryItem = $detail->getInventoryItem(); + $inventoryAcceptedMovement->setInventoryItem($detail->getInventoryItem()); $inventoryAcceptedMovement->toInventoryLocation = $transfer->getDestinationLocation(); $inventoryAcceptedMovement->fromInventoryLocation = $transfer->getDestinationLocation(); // we are moving from incoming to available $inventoryAcceptedMovement->toInventoryTransactionType = InventoryTransactionType::AVAILABLE; @@ -213,9 +213,9 @@ public function actionReceiveTransfer(): Response $inventoryRejectedMovement = new UpdateInventoryLevel(); $inventoryRejectedMovement->quantity = $rejectedAmount * -1; $inventoryRejectedMovement->updateAction = InventoryUpdateQuantityType::ADJUST; - $inventoryRejectedMovement->inventoryItem = $detail->getInventoryItem(); + $inventoryRejectedMovement->inventoryItemId = $detail->inventoryItemId; $inventoryRejectedMovement->transferId = $transfer->id; - $inventoryRejectedMovement->inventoryLocation = $transfer->getDestinationLocation(); + $inventoryRejectedMovement->setInventoryLocation($transfer->getDestinationLocation()); $inventoryRejectedMovement->type = InventoryTransactionType::INCOMING->value; $inventoryUpdateCollection->push($inventoryRejectedMovement); diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php index f59e49e689..e13ed7d21e 100644 --- a/src/elements/Transfer.php +++ b/src/elements/Transfer.php @@ -779,26 +779,26 @@ public function afterSave(bool $isNew): void if ($this->getTransferStatus() === TransferStatusType::PENDING && $originalTransferStatus == TransferStatusType::DRAFT->value) { $inventoryUpdateCollection = new UpdateInventoryLevelCollection(); foreach ($this->getDetails() as $detail) { - $inventoryUpdate1 = new UpdateInventoryLevelInTransfer([ - 'type' => InventoryTransactionType::INCOMING->value, - 'updateAction' => InventoryUpdateQuantityType::ADJUST, - 'inventoryItem' => $detail->getInventoryItem(), - 'transferId' => $this->id, - 'inventoryLocation' => $this->getDestinationLocation(), - 'quantity' => $detail->quantity, - 'note' => Craft::t('commerce', 'Incoming transfer from Transfer ID: ') . $this->id, - ]); + $inventoryUpdate1 = new UpdateInventoryLevelInTransfer(); + $inventoryUpdate1->type = InventoryTransactionType::INCOMING->value; + $inventoryUpdate1->updateAction = InventoryUpdateQuantityType::ADJUST; + $inventoryUpdate1->inventoryItemId = $detail->inventoryItemId; + $inventoryUpdate1->transferId = $this->id; + $inventoryUpdate1->inventoryLocationId = $this->destinationLocationId; + $inventoryUpdate1->quantity = $detail->quantity; + $inventoryUpdate1->note = Craft::t('commerce', 'Incoming transfer from Transfer ID: ') . $this->id; + $inventoryUpdateCollection->push($inventoryUpdate1); - $inventoryUpdate2 = new UpdateInventoryLevelInTransfer([ - 'type' => 'onHand', - 'updateAction' => InventoryUpdateQuantityType::ADJUST, - 'inventoryItem' => $detail->getInventoryItem(), - 'transferId' => $this->id, - 'inventoryLocation' => $this->getOriginLocation(), - 'quantity' => $detail->quantity * -1, - 'note' => Craft::t('commerce', 'Outgoing transfer from Transfer ID: ') . $this->id, - ]); + $inventoryUpdate2 = new UpdateInventoryLevelInTransfer(); + $inventoryUpdate2->type = 'onHand'; + $inventoryUpdate2->updateAction = InventoryUpdateQuantityType::ADJUST; + $inventoryUpdate2->inventoryItemId = $detail->inventoryItemId; + $inventoryUpdate2->transferId = $this->id; + $inventoryUpdate2->inventoryLocationId = $this->originLocationId; + $inventoryUpdate2->quantity = $detail->quantity * -1; + $inventoryUpdate2->note = Craft::t('commerce', 'Outgoing transfer from Transfer ID: ') . $this->id; + $inventoryUpdateCollection->push($inventoryUpdate2); } diff --git a/src/models/inventory/InventoryManualMovement.php b/src/models/inventory/InventoryManualMovement.php index e3e98f5b3d..7d0ed0a7ec 100644 --- a/src/models/inventory/InventoryManualMovement.php +++ b/src/models/inventory/InventoryManualMovement.php @@ -83,7 +83,7 @@ public function fromLocationAfterQuantity(): int ->from(Table::INVENTORYTRANSACTIONS) ->where([ 'type' => $this->fromInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItem->id, + 'inventoryItemId' => $this->inventoryItemId, 'inventoryLocationId' => $this->fromInventoryLocation->id, ]) ->params([':quantity' => $this->quantity]) @@ -112,7 +112,7 @@ public function toLocationAfterQuantity(): int ->from(Table::INVENTORYTRANSACTIONS) ->where([ 'type' => $this->toInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItem->id, + 'inventoryItemId' => $this->inventoryItemId, 'inventoryLocationId' => $this->toInventoryLocation->id, ]) ->params([':quantity' => $this->quantity]) diff --git a/src/models/inventory/InventoryTransferMovement.php b/src/models/inventory/InventoryTransferMovement.php index 558c52cc13..cbe7d8506d 100644 --- a/src/models/inventory/InventoryTransferMovement.php +++ b/src/models/inventory/InventoryTransferMovement.php @@ -9,12 +9,4 @@ */ class InventoryTransferMovement extends InventoryMovement { - /** - * @return array - */ - public function defineRules(): array - { - $rules = parent::defineRules(); - return $rules; - } } diff --git a/src/models/inventory/UpdateInventoryLevel.php b/src/models/inventory/UpdateInventoryLevel.php index 57f8574059..c0e3986279 100644 --- a/src/models/inventory/UpdateInventoryLevel.php +++ b/src/models/inventory/UpdateInventoryLevel.php @@ -3,10 +3,11 @@ namespace craft\commerce\models\inventory; use craft\base\Model; +use craft\commerce\base\InventoryItemTrait; +use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; use craft\commerce\models\InventoryItem; -use craft\commerce\models\InventoryLocation; /** * Update (Set and Adjust) Inventory Quantity model @@ -15,6 +16,8 @@ */ class UpdateInventoryLevel extends Model { + use InventoryItemTrait, InventoryLocationTrait; + /** * The type is the set of InventoryTransactionType values, plus the `onHand` type. * @var string The inventory update type. @@ -32,16 +35,6 @@ class UpdateInventoryLevel extends Model */ public InventoryUpdateQuantityType $updateAction; - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; - - /** - * @var InventoryLocation The inventory location. - */ - public InventoryLocation $inventoryLocation; - /** * @var int The quantity to update. */ diff --git a/src/models/inventory/UpdateInventoryLevelInTransfer.php b/src/models/inventory/UpdateInventoryLevelInTransfer.php index 675e25da1e..553e40eb17 100644 --- a/src/models/inventory/UpdateInventoryLevelInTransfer.php +++ b/src/models/inventory/UpdateInventoryLevelInTransfer.php @@ -3,6 +3,8 @@ namespace craft\commerce\models\inventory; use craft\base\Model; +use craft\commerce\base\InventoryItemTrait; +use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; use craft\commerce\models\InventoryItem; @@ -15,6 +17,8 @@ */ class UpdateInventoryLevelInTransfer extends Model { + use InventoryItemTrait, InventoryLocationTrait; + /** * The type is the set of InventoryTransactionType values, plus the `onHand` type. * @var string The inventory update type. @@ -32,16 +36,6 @@ class UpdateInventoryLevelInTransfer extends Model */ public InventoryUpdateQuantityType $updateAction; - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; - - /** - * @var InventoryLocation The inventory location. - */ - public InventoryLocation $inventoryLocation; - /** * @var int The quantity to update. */ diff --git a/src/services/Inventory.php b/src/services/Inventory.php index 0ffa7726d6..a3324876a0 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -61,7 +61,7 @@ public function getInventoryLevelsForPurchasable(Purchasable $purchasable): Coll $storeInventoryLocations = Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($storeId); foreach ($storeInventoryLocations as $inventoryLocation) { - $inventoryLevel = $this->getInventoryLevel($purchasable->getInventoryItem(), $inventoryLocation); + $inventoryLevel = $this->getInventoryLevel($purchasable->inventoryItemId, $inventoryLocation->id); if (!$inventoryLevel) { continue; @@ -115,17 +115,20 @@ public function getInventoryItemsByIds(array $ids): Collection /** * Returns an inventory level model which is the sum of all inventory movements types for an item in a location. * - * @param InventoryItem $inventoryItem - * @param InventoryLocation $inventoryLocation + * @param InventoryItem|int $inventoryItem + * @param InventoryLocation|int $inventoryLocation * @param bool $withTrashed * @return ?InventoryLevel */ - public function getInventoryLevel(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation, bool $withTrashed = false): ?InventoryLevel + public function getInventoryLevel(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation, bool $withTrashed = false): ?InventoryLevel { + $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; + $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; + $result = $this->getInventoryLevelQuery(withTrashed: $withTrashed) ->andWhere([ - 'inventoryLocationId' => $inventoryLocation->id, - 'inventoryItemId' => $inventoryItem->id, + 'inventoryLocationId' => $inventoryLocationId, + 'inventoryItemId' => $inventoryItemId, ])->one(); if (!$result) { @@ -340,8 +343,8 @@ private function _setInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInT ->from($tableName) ->where([ 'type' => $types, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, ]) ->params([':quantity' => $updateInventoryLevel->quantity]) ->scalar(); @@ -354,8 +357,8 @@ private function _setInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInT $data = [ 'quantity' => $quantityQuery, 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, 'note' => $updateInventoryLevel->note, 'movementHash' => $this->getMovementHash(), 'dateCreated' => Db::prepareDateForDb(new \DateTime()), @@ -389,8 +392,8 @@ private function _adjustInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevel ->insert($tableName, [ 'quantity' => $updateInventoryLevel->quantity, 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, 'movementHash' => $this->getMovementHash(), 'dateCreated' => Db::prepareDateForDb(new \DateTime()), 'note' => $updateInventoryLevel->note, @@ -488,13 +491,16 @@ public function getMovementHash(): string } /** - * @param InventoryItem $inventoryItem - * @param InventoryLocation $inventoryLocation + * @param InventoryItem|int $inventoryItem + * @param InventoryLocation|int $inventoryLocation * @return array */ - public function getUnfulfilledOrders(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation): array + public function getUnfulfilledOrders(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation): array { - $inventoryLevel = $this->getInventoryLevel($inventoryItem, $inventoryLocation); + $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; + $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; + + $inventoryLevel = $this->getInventoryLevel($inventoryItemId, $inventoryLocationId); if ($inventoryLevel->committedTotal <= 0) { return []; @@ -507,8 +513,8 @@ public function getUnfulfilledOrders(InventoryItem $inventoryItem, InventoryLoca ->leftJoin(['orders' => Table::ORDERS], '[[lineItems.orderId]] = [[orders.id]]') ->leftJoin(['it' => Table::INVENTORYTRANSACTIONS], '[[it.lineItemId]] = [[lineItems.id]]') ->where(['orders.isCompleted' => true]) - ->andWhere(['it.inventoryItemId' => $inventoryItem->id]) - ->andWhere(['it.inventoryLocationId' => $inventoryLocation->id]) + ->andWhere(['it.inventoryItemId' => $inventoryItemId]) + ->andWhere(['it.inventoryLocationId' => $inventoryLocationId]) ->andWhere(['it.type' => InventoryTransactionType::COMMITTED->value]) ->groupBy(['lineItems.orderId', 'lineItems.id']) ->having(['>=', 'SUM(it.quantity)', 'lineItems.qty']) @@ -620,6 +626,7 @@ public function getInventoryFulfillmentLevels(Order $order): Collection */ public function orderCompleteHandler(Order $order) { + /** @var Collection[] $allInventoryLevels */ $allInventoryLevels = []; $qtyLineItem = []; foreach ($order->getLineItems() as $lineItem) { @@ -642,7 +649,7 @@ public function orderCompleteHandler(Order $order) $selectedInventoryLevelForItem = []; /** * @var int $purchasableId - * @var InventoryLevel $inventoryLevels + * @var Collection $inventoryLevels */ foreach ($allInventoryLevels as $purchasableId => $inventoryLevels) { foreach ($inventoryLevels as $level) { @@ -685,15 +692,16 @@ public function orderCompleteHandler(Order $order) $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId] -= $lineItem->qty; } - $movements->push(new InventoryCommittedMovement([ - 'inventoryItem' => $level->getInventoryItem(), - 'fromInventoryLocation' => $level->getInventoryLocation(), - 'toInventoryLocation' => $level->getInventoryLocation(), - 'fromInventoryTransactionType' => InventoryTransactionType::AVAILABLE, - 'toInventoryTransactionType' => InventoryTransactionType::COMMITTED, - 'quantity' => $lineItem->qty, - 'lineItemId' => $lineItem->id, - ])); + $inventoryCommittedMovement = new InventoryCommittedMovement(); + $inventoryCommittedMovement->inventoryItemId = $level->inventoryItemId; + $inventoryCommittedMovement->fromInventoryLocation = $level->getInventoryLocation(); + $inventoryCommittedMovement->toInventoryLocation = $level->getInventoryLocation(); + $inventoryCommittedMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; + $inventoryCommittedMovement->toInventoryTransactionType = InventoryTransactionType::COMMITTED; + $inventoryCommittedMovement->quantity = $lineItem->qty; + $inventoryCommittedMovement->lineItemId = $lineItem->id; + + $movements->push($inventoryCommittedMovement); } } @@ -718,15 +726,16 @@ public function orderCompleteHandler(Order $order) $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId] -= $qtyToReserve; - $movements->push(new InventoryManualMovement([ - 'inventoryItem' => $level->getInventoryItem(), - 'fromInventoryLocation' => $level->getInventoryLocation(), - 'toInventoryLocation' => $level->getInventoryLocation(), - 'fromInventoryTransactionType' => InventoryTransactionType::AVAILABLE, - 'toInventoryTransactionType' => InventoryTransactionType::RESERVED, - 'quantity' => $qtyToReserve, - 'lineItemId' => $lineItemId, - ])); + $inventoryManualMovement = new InventoryManualMovement(); + $inventoryManualMovement->inventoryItemId = $level->inventoryItemId; + $inventoryManualMovement->fromInventoryLocation = $level->getInventoryLocation(); + $inventoryManualMovement->toInventoryLocation = $level->getInventoryLocation(); + $inventoryManualMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; + $inventoryManualMovement->toInventoryTransactionType = InventoryTransactionType::RESERVED; + $inventoryManualMovement->quantity = $qtyToReserve; + $inventoryManualMovement->lineItemId = $lineItemId; + + $movements->push($inventoryManualMovement); $qty -= $qtyToReserve; if ($qty <= 0) { diff --git a/src/services/InventoryLocations.php b/src/services/InventoryLocations.php index 31ead4e14f..c5d4375c81 100644 --- a/src/services/InventoryLocations.php +++ b/src/services/InventoryLocations.php @@ -180,7 +180,7 @@ public function executeDeactivateInventoryLocation(DeactivateInventoryLocation $ $inventoryMovement = new InventoryLocationDeactivatedMovement(); $inventoryMovement->fromInventoryLocation = $deactivateInventoryLocation->inventoryLocation; $inventoryMovement->toInventoryLocation = $deactivateInventoryLocation->destinationInventoryLocation; - $inventoryMovement->inventoryItem = $inventoryLevel->getInventoryItem(); + $inventoryMovement->inventoryItemId = $inventoryLevel->inventoryItemId; $inventoryMovement->quantity = $inventoryLevel->getTotal($type); $inventoryMovement->fromInventoryTransactionType = $type; $inventoryMovement->toInventoryTransactionType = $type; From 78e1f4b436151128ec194a3c8a2ecd690f1d600f Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 14:51:17 +0000 Subject: [PATCH 29/44] Tidy --- src/base/InventoryMovement.php | 1 - src/models/inventory/UpdateInventoryLevel.php | 1 - src/models/inventory/UpdateInventoryLevelInTransfer.php | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/base/InventoryMovement.php b/src/base/InventoryMovement.php index f858d301a1..6d8ea1b6d3 100644 --- a/src/base/InventoryMovement.php +++ b/src/base/InventoryMovement.php @@ -8,7 +8,6 @@ namespace craft\commerce\base; use craft\commerce\enums\InventoryTransactionType; -use craft\commerce\models\InventoryItem; use craft\commerce\models\InventoryLocation; /** diff --git a/src/models/inventory/UpdateInventoryLevel.php b/src/models/inventory/UpdateInventoryLevel.php index c0e3986279..38ac79cd63 100644 --- a/src/models/inventory/UpdateInventoryLevel.php +++ b/src/models/inventory/UpdateInventoryLevel.php @@ -7,7 +7,6 @@ use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; -use craft\commerce\models\InventoryItem; /** * Update (Set and Adjust) Inventory Quantity model diff --git a/src/models/inventory/UpdateInventoryLevelInTransfer.php b/src/models/inventory/UpdateInventoryLevelInTransfer.php index 553e40eb17..e85aee3cd9 100644 --- a/src/models/inventory/UpdateInventoryLevelInTransfer.php +++ b/src/models/inventory/UpdateInventoryLevelInTransfer.php @@ -7,8 +7,6 @@ use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; -use craft\commerce\models\InventoryItem; -use craft\commerce\models\InventoryLocation; /** * Update (Set and Adjust) Inventory Quantity model From f5ebebf2b901d5902d64e95d57a139f84bc2c100 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 14:51:31 +0000 Subject: [PATCH 30/44] Add `Purchasable::setStockLevel()` --- CHANGELOG-WIP.md | 9 +- src/base/Purchasable.php | 82 +++++----------- .../elements/variant/VariantStockTest.php | 93 +++++++++++++++++++ 3 files changed, 126 insertions(+), 58 deletions(-) create mode 100644 tests/unit/elements/variant/VariantStockTest.php diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index c9e510bf7d..cd6c262b92 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,7 +1,14 @@ # Release Notes for Craft Commerce (WIP) -## Administration +### Administration - Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738)) - Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430)) - Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) + + +### System + +- Added `craft\commerce\base\InventoryItemTrait`. +- Added `craft\commerce\base\InventoryLocationTrait`. +- Added `craft\commerce\base\Purchasable::setStockLevel()`. \ No newline at end of file diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 1701e4981e..3c88c51113 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -13,6 +13,8 @@ use craft\commerce\collections\UpdateInventoryLevelCollection; use craft\commerce\db\Table; use craft\commerce\elements\Order; +use craft\commerce\enums\InventoryTransactionType; +use craft\commerce\enums\InventoryUpdateQuantityType; use craft\commerce\errors\StoreNotFoundException; use craft\commerce\helpers\Currency; use craft\commerce\helpers\Localization; @@ -44,6 +46,7 @@ use Money\Money; use Money\Teller; use yii\base\InvalidConfigException; +use yii\db\Exception; use yii\validators\Validator; /** @@ -952,67 +955,32 @@ public function getStock(): int } /** + * @param int $quantity + * @param array $updateAttributes * @return void + * @throws InvalidConfigException + * @throws Exception * @since 5.3.0 */ - public function setStockLevel($amount): void - { - - // $commercePlugin = Plugin::getInstance(); - // - // // For simplicity let's just get the first inventory location - // $inventoryLocation = $commercePlugin->getInventoryLocations()->getAllInventoryLocations()->first(); - // - // // Retrieve the inventory item ID for the variants you want to update - // $variantInventoryItemQuery = \craft\commerce\elements\Variant::find() - // // ->select(['inventoryitems.id as inventoryItemId']); - // ->select(['inventoryitems.id as inventoryItemId']) - // ->andWhere(['elements.id' => 327]); - // // Can add extra criteria to the query if needed - // - // // Retrieve the inventory items for the variants - // $inventoryItems = $commercePlugin->getInventory()->getInventoryItemQuery() - // ->andWhere(['id' => $variantInventoryItemQuery]) - // ->collect() - // ->map(function($inventoryItem) { - // return new InventoryItem($inventoryItem); - // }); - // - // // Retrieve the inventory levels for the inventory items - // $inventoryLevels = $commercePlugin->getInventory()->getInventoryLevelQuery() - // ->andWhere(['ii.id' => $variantInventoryItemQuery]) - // ->andWhere(['it.inventoryLocationId' => $inventoryLocation->id]) - // ->collect() - // ->map(function($inventoryLevel) { - // unset($inventoryLevel['purchasableId']); - // return new InventoryLevel($inventoryLevel); - // }); - // - // $updateInventoryLevels = UpdateInventoryLevelCollection::make(); - // - // $inventoryLevels->each(function(InventoryLevel $inventoryLevel) use ($updateInventoryLevels, $inventoryItems, $inventoryLocation) { - // // Update action can be `SET` or `ADJUST` based on inserting or updating the stock - // $updateAction = \craft\commerce\enums\InventoryUpdateQuantityType::SET; - // - // // Stock amount, this would need to be based on your requirements/data being imported - // $stock = 1; // stock amount - // - // $updateInventoryLevels->push(new UpdateInventoryLevel([ - // 'type' => InventoryTransactionType::AVAILABLE->value, - // 'updateAction' => $updateAction, - // 'inventoryItem' => $inventoryItems->firstWhere('id', $inventoryLevel->inventoryItemId), - // 'inventoryLocation' => $inventoryLocation, - // 'quantity' => $stock, - // 'note' => '', - // ]) - // ); - // }); - // - // if ($updateInventoryLevels->count() > 0) { - // $commercePlugin->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); - // } - } + public function setStockLevel(int $quantity, array $updateAttributes = []): void + { + $updateAttributes += [ + 'quantity' => $quantity, + 'updateAction' => InventoryUpdateQuantityType::SET, + 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, + 'type' => InventoryTransactionType::AVAILABLE->value, + ]; + $updateInventoryLevel = new UpdateInventoryLevel($updateAttributes); + $updateInventoryLevel->inventoryItemId = $this->inventoryItemId; + + $updateInventoryLevels = UpdateInventoryLevelCollection::make(); + $updateInventoryLevels->push($updateInventoryLevel); + + Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); + + $this->_stock = null; + } /** * Returns the total stock across all locations this purchasable is tracked in. diff --git a/tests/unit/elements/variant/VariantStockTest.php b/tests/unit/elements/variant/VariantStockTest.php new file mode 100644 index 0000000000..de710590d7 --- /dev/null +++ b/tests/unit/elements/variant/VariantStockTest.php @@ -0,0 +1,93 @@ + + * @since 5.3.0 + */ +class VariantStockTest extends Unit +{ + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'products' => [ + 'class' => ProductFixture::class, + ], + ]; + } + + /** + * @param array $updateConfigs + * @return void + * @throws \yii\base\InvalidConfigException + * @throws \yii\db\Exception + * @dataProvider setStockLevelDataProvider + */ + public function testSetStockLevel(array $updateConfigs, int $expected): void + { + $variant = Variant::find()->sku('rad-hood')->one(); + $originalStock = $variant->getStock(); + + foreach ($updateConfigs as $updateConfig) { + $qty = $updateConfig['quantity']; + unset($updateConfig['quantity']); + + $variant->setStockLevel($qty, $updateConfig); + } + + self::assertEquals($expected, $variant->getStock()); + + $variant->setStockLevel($originalStock); + } + + /** + * @return array[] + */ + public function setStockLevelDataProvider(): array + { + return [ + 'simple-single-arg' => [ + [ + ['quantity' => 10], + ], + 'expected' => 10, + ], + 'set-and-adjust' => [ + [ + ['quantity' => 10], + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 12, + ], + 'just-adjust' => [ + [ + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 2, + ], + 'set-and-adjust-negative' => [ + [ + ['quantity' => 10], + ['quantity' => -2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 8, + ], + ]; + } +} From f9b4fae277444862a8f0a3320b5835ad890f0005 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 15:06:42 +0000 Subject: [PATCH 31/44] Added `Iventory::updateInventoryLevel()` --- CHANGELOG-WIP.md | 3 ++- src/base/Purchasable.php | 19 +++---------------- src/services/Inventory.php | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index cd6c262b92..90f6838d32 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -11,4 +11,5 @@ - Added `craft\commerce\base\InventoryItemTrait`. - Added `craft\commerce\base\InventoryLocationTrait`. -- Added `craft\commerce\base\Purchasable::setStockLevel()`. \ No newline at end of file +- Added `craft\commerce\base\Purchasable::setStockLevel()`. +- Added `craft\commerce\services\Inventory::updateInventoryLevel()`. \ No newline at end of file diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 3c88c51113..2c094b4c77 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -956,28 +956,15 @@ public function getStock(): int /** * @param int $quantity - * @param array $updateAttributes + * @param array $updateInventoryLevelAttributes * @return void * @throws InvalidConfigException * @throws Exception * @since 5.3.0 */ - public function setStockLevel(int $quantity, array $updateAttributes = []): void + public function setStockLevel(int $quantity, array $updateInventoryLevelAttributes = []): void { - $updateAttributes += [ - 'quantity' => $quantity, - 'updateAction' => InventoryUpdateQuantityType::SET, - 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, - 'type' => InventoryTransactionType::AVAILABLE->value, - ]; - - $updateInventoryLevel = new UpdateInventoryLevel($updateAttributes); - $updateInventoryLevel->inventoryItemId = $this->inventoryItemId; - - $updateInventoryLevels = UpdateInventoryLevelCollection::make(); - $updateInventoryLevels->push($updateInventoryLevel); - - Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); + Plugin::getInstance()->getInventory()->updateInventoryLevel($this->inventoryItemId, $quantity, $updateInventoryLevelAttributes); $this->_stock = null; } diff --git a/src/services/Inventory.php b/src/services/Inventory.php index a3324876a0..0dc088b619 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -325,6 +325,32 @@ public function executeUpdateInventoryLevels(UpdateInventoryLevelCollection $upd } } + /** + * @param int $inventoryItemId + * @param int $quantity + * @param array $updateInventoryLevelAttributes + * @return void + * @throws Exception + * @throws InvalidConfigException + */ + public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes = []) + { + $updateInventoryLevelAttributes += [ + 'quantity' => $quantity, + 'updateAction' => InventoryUpdateQuantityType::SET, + 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, + 'type' => InventoryTransactionType::AVAILABLE->value, + ]; + + $updateInventoryLevel = new UpdateInventoryLevel($updateInventoryLevelAttributes); + $updateInventoryLevel->inventoryItemId = $inventoryItemId; + + $updateInventoryLevels = UpdateInventoryLevelCollection::make(); + $updateInventoryLevels->push($updateInventoryLevel); + + Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); + } + /** * @param UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel * @return bool From 026b521f0cd83cb8303defbabb089d12fcf22237 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 15:15:35 +0000 Subject: [PATCH 32/44] fix cs --- src/base/Purchasable.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 2c094b4c77..40ecab5f13 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -10,16 +10,12 @@ use Craft; use craft\base\Element; use craft\base\NestedElementInterface; -use craft\commerce\collections\UpdateInventoryLevelCollection; use craft\commerce\db\Table; use craft\commerce\elements\Order; -use craft\commerce\enums\InventoryTransactionType; -use craft\commerce\enums\InventoryUpdateQuantityType; use craft\commerce\errors\StoreNotFoundException; use craft\commerce\helpers\Currency; use craft\commerce\helpers\Localization; use craft\commerce\helpers\Purchasable as PurchasableHelper; -use craft\commerce\models\inventory\UpdateInventoryLevel; use craft\commerce\models\InventoryItem; use craft\commerce\models\InventoryLevel; use craft\commerce\models\LineItem; From 4f007faae0ae3475bd6ddfdbc8fb49b509195e46 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 15:17:45 +0000 Subject: [PATCH 33/44] fix default location --- src/base/Purchasable.php | 4 ++++ src/services/Inventory.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 40ecab5f13..ae4e204e84 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -960,6 +960,10 @@ public function getStock(): int */ public function setStockLevel(int $quantity, array $updateInventoryLevelAttributes = []): void { + $updateInventoryLevelAttributes += [ + 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, + ]; + Plugin::getInstance()->getInventory()->updateInventoryLevel($this->inventoryItemId, $quantity, $updateInventoryLevelAttributes); $this->_stock = null; diff --git a/src/services/Inventory.php b/src/services/Inventory.php index 0dc088b619..a1f2ae4769 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -338,7 +338,7 @@ public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes += [ 'quantity' => $quantity, 'updateAction' => InventoryUpdateQuantityType::SET, - 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, + 'inventoryLocationId' => Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations()->first()->id, 'type' => InventoryTransactionType::AVAILABLE->value, ]; From 99bb3f8400c473e5dc9ae543cba631a4a096e156 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 15:38:26 +0000 Subject: [PATCH 34/44] Add exception to stop being able to set stock level on an unsaved purchasable --- src/base/Purchasable.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index ae4e204e84..af58288e1b 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -960,6 +960,10 @@ public function getStock(): int */ public function setStockLevel(int $quantity, array $updateInventoryLevelAttributes = []): void { + if ($this->inventoryItemId === null) { + throw new InvalidConfigException('Cannot set stock level on a purchasable without an inventory item.'); + } + $updateInventoryLevelAttributes += [ 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, ]; From 134d83c5c5d843246d4983c8a0af02d2e526d1c7 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Thu, 5 Dec 2024 15:42:13 +0000 Subject: [PATCH 35/44] Tidy [ci skip] --- src/elements/Transfer.php | 68 +++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php index e13ed7d21e..b6da0accef 100644 --- a/src/elements/Transfer.php +++ b/src/elements/Transfer.php @@ -191,7 +191,7 @@ public function setTransferStatus(TransferStatusType|string $status): void } /** - * @inheritDoc + * @inheritdoc */ public static function displayName(): string { @@ -199,7 +199,7 @@ public static function displayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function lowerDisplayName(): string { @@ -207,7 +207,7 @@ public static function lowerDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function pluralDisplayName(): string { @@ -215,7 +215,7 @@ public static function pluralDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function pluralLowerDisplayName(): string { @@ -223,7 +223,7 @@ public static function pluralLowerDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function refHandle(): ?string { @@ -231,7 +231,7 @@ public static function refHandle(): ?string } /** - * @inheritDoc + * @inheritdoc */ public static function trackChanges(): bool { @@ -239,7 +239,7 @@ public static function trackChanges(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasTitles(): bool { @@ -247,7 +247,7 @@ public static function hasTitles(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasContent(): bool { @@ -255,7 +255,7 @@ public static function hasContent(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasUris(): bool { @@ -263,7 +263,7 @@ public static function hasUris(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function isLocalized(): bool { @@ -271,7 +271,7 @@ public static function isLocalized(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasStatuses(): bool { @@ -280,7 +280,7 @@ public static function hasStatuses(): bool /** * @return TransferQuery - * @inheritDoc + * @inheritdoc */ public static function find(): ElementQueryInterface { @@ -288,7 +288,7 @@ public static function find(): ElementQueryInterface } /** - * @inheritDoc + * @inheritdoc */ public static function createCondition(): ElementConditionInterface { @@ -296,7 +296,7 @@ public static function createCondition(): ElementConditionInterface } /** - * @inheritDoc + * @inheritdoc */ protected static function includeSetStatusAction(): bool { @@ -331,7 +331,7 @@ protected static function defineSortOptions(): array } /** - * @inheritDoc + * @inheritdoc */ protected static function defineTableAttributes(): array { @@ -347,7 +347,7 @@ protected static function defineTableAttributes(): array } /** - * @inheritDoc + * @inheritdoc */ protected static function defineDefaultTableAttributes(string $source): array { @@ -359,7 +359,7 @@ protected static function defineDefaultTableAttributes(string $source): array } /** - * @inheritDoc + * @inheritdoc */ protected function attributeHtml(string $attribute): string { @@ -384,7 +384,7 @@ protected function attributeHtml(string $attribute): string } /** - * @inheritDoc + * @inheritdoc */ protected function defineRules(): array { @@ -436,7 +436,7 @@ public function validateLocations($attribute, $params, $validator) } /** - * @inheritDoc + * @inheritdoc */ public function getUriFormat(): ?string { @@ -480,7 +480,7 @@ protected static function defineSources(string $context = null): array /** * - * @inheritDoc + * @inheritdoc */ protected function previewTargets(): array { @@ -497,6 +497,10 @@ protected function previewTargets(): array return $previewTargets; } + + /** + * @inheritdoc + */ protected function safeActionMenuItems(): array { $safeActions = parent::safeActionMenuItems(); @@ -518,7 +522,7 @@ protected function safeActionMenuItems(): array /** - * @inheritDoc + * @inheritdoc */ protected function route(): array|string|null { @@ -533,7 +537,7 @@ protected function route(): array|string|null } /** - * @inheritDoc + * @inheritdoc */ public function canView(User $user): bool { @@ -545,7 +549,7 @@ public function canView(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canSave(User $user): bool { @@ -557,7 +561,7 @@ public function canSave(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canDuplicate(User $user): bool { @@ -565,7 +569,7 @@ public function canDuplicate(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canDelete(User $user): bool { @@ -583,7 +587,7 @@ public function canDelete(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canCreateDrafts(User $user): bool { @@ -591,7 +595,7 @@ public function canCreateDrafts(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ protected function cpEditUrl(): ?string { @@ -599,7 +603,7 @@ protected function cpEditUrl(): ?string } /** - * @inheritDoc + * @inheritdoc */ public function getPostEditUrl(): ?string { @@ -607,7 +611,7 @@ public function getPostEditUrl(): ?string } /** - * @inheritDoc + * @inheritdoc */ public function prepareEditScreen(Response $response, string $containerId): void { @@ -735,7 +739,7 @@ public function addDetail(TransferDetail $detail): void } /** - * @inheritDoc + * @inheritdoc */ public function getFieldLayout(): ?FieldLayout { @@ -743,7 +747,7 @@ public function getFieldLayout(): ?FieldLayout } /** - * @inheritDoc + * @inheritdoc */ public function beforeValidate() { @@ -755,7 +759,7 @@ public function beforeValidate() } /** - * @inheritDoc + * @inheritdoc */ public function afterSave(bool $isNew): void { From 4ffa9b671f7a72a742208384c16ccf3e33c56f0d Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 6 Dec 2024 09:47:59 +0000 Subject: [PATCH 36/44] Switch to just using service method for updating purchasable inventory level --- CHANGELOG-WIP.md | 4 +- src/base/Purchasable.php | 37 +++++++------------ src/elements/Transfer.php | 8 +++- src/services/Inventory.php | 27 ++++++++++++++ .../InventoryTest.php} | 22 +++++++---- 5 files changed, 63 insertions(+), 35 deletions(-) rename tests/unit/{elements/variant/VariantStockTest.php => services/InventoryTest.php} (75%) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 90f6838d32..39fcf07f8c 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -11,5 +11,5 @@ - Added `craft\commerce\base\InventoryItemTrait`. - Added `craft\commerce\base\InventoryLocationTrait`. -- Added `craft\commerce\base\Purchasable::setStockLevel()`. -- Added `craft\commerce\services\Inventory::updateInventoryLevel()`. \ No newline at end of file +- Added `craft\commerce\services\Inventory::updateInventoryLevel()`. +- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`. \ No newline at end of file diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index af58288e1b..a30ad96584 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -384,6 +384,20 @@ private function _getTeller(): Teller return Plugin::getInstance()->getCurrencies()->getTeller($this->getStore()->getCurrency()); } + /** + * @inheritdoc + */ + public function __unset($name) + { + // Allow clearing of specific memoized properties + if (in_array($name, ['stock', 'shippingCategory', 'taxCategory'])) { + $this->{'_' . $name} = null; + return; + } + + parent::__unset($name); + } + /** * @inheritdoc */ @@ -950,29 +964,6 @@ public function getStock(): int return $this->_stock; } - /** - * @param int $quantity - * @param array $updateInventoryLevelAttributes - * @return void - * @throws InvalidConfigException - * @throws Exception - * @since 5.3.0 - */ - public function setStockLevel(int $quantity, array $updateInventoryLevelAttributes = []): void - { - if ($this->inventoryItemId === null) { - throw new InvalidConfigException('Cannot set stock level on a purchasable without an inventory item.'); - } - - $updateInventoryLevelAttributes += [ - 'inventoryLocationId' => $this->getStore()->getInventoryLocations()->first()->id, - ]; - - Plugin::getInstance()->getInventory()->updateInventoryLevel($this->inventoryItemId, $quantity, $updateInventoryLevelAttributes); - - $this->_stock = null; - } - /** * Returns the total stock across all locations this purchasable is tracked in. * @return Collection diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php index b6da0accef..df1b16120e 100644 --- a/src/elements/Transfer.php +++ b/src/elements/Transfer.php @@ -82,11 +82,17 @@ public function __toString(): string ]); } + /** + * @inheritdoc + */ public static function hasDrafts(): bool { return false; } + /** + * @inheritdoc + */ protected function metadata(): array { $additionalMeta = []; @@ -497,7 +503,6 @@ protected function previewTargets(): array return $previewTargets; } - /** * @inheritdoc */ @@ -520,7 +525,6 @@ protected function safeActionMenuItems(): array return $safeActions; } - /** * @inheritdoc */ diff --git a/src/services/Inventory.php b/src/services/Inventory.php index a1f2ae4769..9150a2f392 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -332,6 +332,7 @@ public function executeUpdateInventoryLevels(UpdateInventoryLevelCollection $upd * @return void * @throws Exception * @throws InvalidConfigException + * @since 5.3.0 */ public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes = []) { @@ -351,6 +352,32 @@ public function updateInventoryLevel(int $inventoryItemId, int $quantity, array Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); } + /** + * @param Purchasable $purchasable + * @param int $quantity + * @param array $updateInventoryLevelAttributes + * @return void + * @throws Exception + * @throws InvalidConfigException + * @throws \craft\errors\DeprecationException + * @since 5.3.0 + */ + public function updatePurchasableInventoryLevel(Purchasable $purchasable, int $quantity, array $updateInventoryLevelAttributes = []) + { + $updateInventoryLevelAttributes += [ + 'quantity' => $quantity, + 'updateAction' => InventoryUpdateQuantityType::SET, + 'inventoryItemId' => $purchasable->inventoryItemId, + 'inventoryLocationId' => $purchasable->getStore()->getInventoryLocations()->first()->id, + 'type' => InventoryTransactionType::AVAILABLE->value, + ]; + + $this->updateInventoryLevel($purchasable->inventoryItemId, $quantity, $updateInventoryLevelAttributes); + + // Clear the stock cache for the class instance + unset($purchasable->stock); + } + /** * @param UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel * @return bool diff --git a/tests/unit/elements/variant/VariantStockTest.php b/tests/unit/services/InventoryTest.php similarity index 75% rename from tests/unit/elements/variant/VariantStockTest.php rename to tests/unit/services/InventoryTest.php index de710590d7..78ae95fb76 100644 --- a/tests/unit/elements/variant/VariantStockTest.php +++ b/tests/unit/services/InventoryTest.php @@ -5,20 +5,24 @@ * @license https://craftcms.github.io/license/ */ -namespace unit\elements\variant; +namespace craftcommercetests\unit\services; use Codeception\Test\Unit; use craft\commerce\elements\Variant; use craft\commerce\enums\InventoryUpdateQuantityType; +use craft\commerce\Plugin; +use craft\errors\DeprecationException; use craftcommercetests\fixtures\ProductFixture; +use yii\base\InvalidConfigException; +use yii\db\Exception; /** - * VariantStockTest + * InventoryTest * * @author Pixel & Tonic, Inc. * @since 5.3.0 */ -class VariantStockTest extends Unit +class InventoryTest extends Unit { /** * @return array @@ -34,12 +38,14 @@ public function _fixtures(): array /** * @param array $updateConfigs + * @param int $expected * @return void - * @throws \yii\base\InvalidConfigException - * @throws \yii\db\Exception + * @throws DeprecationException + * @throws InvalidConfigException + * @throws Exception * @dataProvider setStockLevelDataProvider */ - public function testSetStockLevel(array $updateConfigs, int $expected): void + public function testUpdatePurchasableInventoryLevel(array $updateConfigs, int $expected): void { $variant = Variant::find()->sku('rad-hood')->one(); $originalStock = $variant->getStock(); @@ -48,12 +54,12 @@ public function testSetStockLevel(array $updateConfigs, int $expected): void $qty = $updateConfig['quantity']; unset($updateConfig['quantity']); - $variant->setStockLevel($qty, $updateConfig); + Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $qty, $updateConfig); } self::assertEquals($expected, $variant->getStock()); - $variant->setStockLevel($originalStock); + Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $originalStock); } /** From f29a51acea3d429a928a842f601d62313f3f20cd Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 6 Dec 2024 09:51:10 +0000 Subject: [PATCH 37/44] Fix cs --- src/base/Purchasable.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index a30ad96584..6f7f7feee5 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -42,7 +42,6 @@ use Money\Money; use Money\Teller; use yii\base\InvalidConfigException; -use yii\db\Exception; use yii\validators\Validator; /** From 12e7920fe224ea9c720eeb246b7b5e65f70544b0 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Fri, 6 Dec 2024 16:04:27 +0000 Subject: [PATCH 38/44] Fixed errors not showing on order edit after saving --- CHANGELOG.md | 4 ++++ src/controllers/OrdersController.php | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b86159db3..c2030227c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes for Craft Commerce +## Unreleased + +- Fixed a bug where order errors weren't showing on the Edit Order page. + ## 4.7.1 - 2024-12-02 - Fixed an error that occurred on the Orders index page when running Craft CMS 4.13.4 or later. ([#3793](https://github.com/craftcms/commerce/issues/3793)) diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 0a7533352f..a35dfc37db 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -1251,6 +1251,7 @@ private function _registerJavascript(array $variables): void if ($variables['order']->hasErrors()) { $response['order']['errors'] = $variables['order']->getErrors(); + $response['errors'] = $variables['order']->getErrors(); $response['error'] = Craft::t('commerce', 'The order is not valid.'); } From 206b1f8ff1b8b3783f5813673973beaec180571d Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 10 Dec 2024 08:39:59 +0000 Subject: [PATCH 39/44] #3807 default price showing incorrectly for products in CP --- CHANGELOG.md | 1 + src/elements/Variant.php | 1 + src/elements/db/ProductQuery.php | 14 ++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93168ab77d..8a8f17cd62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Fixed a bug where a product’s default price was showing incorrectly on the Products index page. ([#3807](https://github.com/craftcms/commerce/issues/3807)) - Fixed a bug where order errors weren't showing on the Edit Order page. ## 5.2.8 - 2024-12-04 diff --git a/src/elements/Variant.php b/src/elements/Variant.php index 1ab9f48363..a556e1c0b0 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -1068,6 +1068,7 @@ public function afterSave(bool $isNew): void parent::afterSave($isNew); if (!$this->propagating && $this->isDefault && $ownerId && $this->duplicateOf === null) { + // @TODO - this data is now joined in on the product query so can be removed at the next breaking change $defaultData = [ 'defaultVariantId' => $this->id, 'defaultSku' => $this->getSkuAsText(), diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index 28be43d670..745a542cf7 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -768,18 +768,20 @@ protected function beforePrepare(): bool 'commerce_products.postDate', 'commerce_products.expiryDate', 'subquery.price as defaultPrice', - 'commerce_products.defaultPrice as defaultBasePrice', + 'purchasablesstores.basePrice as defaultBasePrice', 'commerce_products.defaultVariantId', - 'commerce_products.defaultSku', - 'commerce_products.defaultWeight', - 'commerce_products.defaultLength', - 'commerce_products.defaultWidth', - 'commerce_products.defaultHeight', + 'purchasables.sku as defaultSku', + 'purchasables.weight as defaultWeight', + 'purchasables.length as defaultLength', + 'purchasables.width as defaultWidth', + 'purchasables.height as defaultHeight', 'sitestores.storeId', ]); // Join in sites stores to get product's store for current request $this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); + $this->query->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); + $this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]'); $this->subQuery->addSelect(['catalogprices.price']); From cdb042611602110cd18a6216d4032fc6f65a6acb Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 10 Dec 2024 12:03:38 +0000 Subject: [PATCH 40/44] #3805 Inline-editable matrix fields not saving on variants Co-authored-by: Iwona Just --- CHANGELOG.md | 1 + src/base/Purchasable.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8f17cd62..db68d6a985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Fixed a bug where a product’s default price was showing incorrectly on the Products index page. ([#3807](https://github.com/craftcms/commerce/issues/3807)) +- Fixed a bug where inline-editable Matrix fields weren’t saving content on product variants. ([#3805](https://github.com/craftcms/commerce/issues/3805)) - Fixed a bug where order errors weren't showing on the Edit Order page. ## 5.2.8 - 2024-12-04 diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index ce7906982c..764fbc4eb4 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -1119,6 +1119,8 @@ public function afterSave(bool $isNew): void */ public function afterPropagate(bool $isNew): void { + parent::afterPropagate($isNew); + Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ 'purchasableIds' => [$this->getCanonicalId()], 'storeId' => $this->getStoreId(), From 0d7ec5ffd7ab0584a2c48c73627f13571a86b96a Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 10 Dec 2024 12:40:02 +0000 Subject: [PATCH 41/44] Move card attributes to purchasable class --- src/base/Purchasable.php | 95 ++++++++++++++++++++++++++++++++++++++++ src/elements/Variant.php | 88 +++++-------------------------------- 2 files changed, 107 insertions(+), 76 deletions(-) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 764fbc4eb4..2a95d0fd36 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -1276,6 +1276,24 @@ protected function attributeHtml(string $attribute): string } } + $dimensions = []; + if ($attribute === 'dimensions') { + $dimensions = array_filter([ + $this->length, + $this->width, + $this->height + ]); + } + + if ($attribute === 'priceView') { + $price = $this->basePriceAsCurrency; + if ($this->getBasePromotionalPrice() && $this->getBasePromotionalPrice() < $this->getBasePrice()) { + $price = Html::tag('del', $price, ['style' => 'opacity: .5']) . ' ' . $this->basePromotionalPriceAsCurrency; + } + + return $price; + } + return match ($attribute) { 'sku' => (string)Html::encode($this->getSkuAsText()), 'price' => $this->basePriceAsCurrency, @@ -1287,6 +1305,7 @@ protected function attributeHtml(string $attribute): string 'minQty' => (string)$this->minQty, 'maxQty' => (string)$this->maxQty, 'stock' => $stock, + 'dimensions' => !empty($dimensions) ? implode(' x ', $dimensions) . ' ' . Plugin::getInstance()->getSettings()->dimensionUnits : '', default => parent::attributeHtml($attribute), }; } @@ -1324,6 +1343,82 @@ protected static function defineDefaultTableAttributes(string $source): array ]; } + /** + * @inheritdoc + */ + public static function attributePreviewHtml(array $attribute): mixed + { + return match($attribute['value']) { + 'sku', 'priceView', 'dimensions', 'weight' => $attribute['placeholder'], + 'availableForPurchase', 'promotable' => Html::tag('span', '', [ + 'class' => 'checkbox-icon', + 'role' => 'img', + 'title' => $attribute['label'], + 'aria' => [ + 'label' => $attribute['label'], + ], + ]) . + Html::tag('span', $attribute['label'], [ + 'class' => 'checkbox-preview-label', + ]), + default => parent::attributePreviewHtml($attribute) + }; + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'sku', + 'priceView', + ]); + } + + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(Element::defineCardAttributes(), [ + 'availableForPurchase' => [ + 'label' => Craft::t('commerce', 'Available for purchase'), + ], + 'basePrice' => [ + 'label' => Craft::t('commerce', 'Base Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'basePromotionalPrice' => [ + 'label' => Craft::t('commerce', 'Base Promotional Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'dimensions' => [ + 'label' => Craft::t('commerce', 'Dimensions'), + 'placeholder' => '1 x 2 x 3 ' . Plugin::getInstance()->getSettings()->dimensionUnits, + ], + 'priceView' => [ + 'label' => Craft::t('commerce', 'Price'), + 'placeholder' => Html::tag('del', '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(199.99), ['style' => 'opacity: .5']) . ' ¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'promotable' => [ + 'label' => Craft::t('commerce', 'Promotable'), + ], + 'sku' => [ + 'label' => Craft::t('commerce', 'SKU'), + 'placeholder' => Html::tag('code', 'SKU123'), + ], + 'stock' => [ + 'label' => Craft::t('commerce', 'Stock'), + 'placeholder' => 10, + ], + 'weight' => [ + 'label' => Craft::t('commerce', 'Weight'), + 'placeholder' => 123 . Plugin::getInstance()->getSettings()->weightUnits, + ] + ]); + } + /** * @inheritdoc */ diff --git a/src/elements/Variant.php b/src/elements/Variant.php index 6204d5307d..7ef4f42117 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -1338,73 +1338,6 @@ protected static function defineActions(string $source): array ]]; } - /** - * @inheritdoc - */ - public static function attributePreviewHtml(array $attribute): mixed - { - return match($attribute['value']) { - 'sku', 'priceView' => $attribute['placeholder'], - 'availableForPurchase', 'promotable' => Html::tag('span', '', [ - 'class' => 'checkbox-icon', - 'role' => 'img', - 'title' => $attribute['label'], - 'aria' => [ - 'label' => $attribute['label'], - ], - ]) . - Html::tag('span', $attribute['label'], [ - 'class' => 'checkbox-preview-label', - ]), - default => parent::attributePreviewHtml($attribute) - }; - } - - /** - * @inheritdoc - */ - protected static function defineCardAttributes(): array - { - return array_merge(parent::defineCardAttributes(), [ - 'basePromotionalPrice' => [ - 'label' => Craft::t('commerce', 'Base Promotional Price'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'basePrice' => [ - 'label' => Craft::t('commerce', 'Base Price'), - 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'product' => [ - 'label' => Craft::t('commerce', 'Product'), - ], - 'promotable' => [ - 'label' => Craft::t('commerce', 'Promotable'), - ], - 'availableForPurchase' => [ - 'label' => Craft::t('commerce', 'Available for purchase'), - ], - 'priceView' => [ - 'label' => Craft::t('commerce', 'Price'), - 'placeholder' => Html::tag('del','¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(199.99), ['style' => 'opacity: .5']) . ' ¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), - ], - 'sku' => [ - 'label' => Craft::t('commerce', 'SKU'), - 'placeholder' => Html::tag('code', 'SKU123'), - ], - ]); - } - - /** - * @inheritdoc - */ - protected static function defineDefaultCardAttributes(): array - { - return array_merge(parent::defineDefaultCardAttributes(), [ - 'sku', - 'priceView', - ]); - } - /** * @inheritdoc */ @@ -1439,6 +1372,18 @@ protected static function defineSearchableAttributes(): array return [...parent::defineSearchableAttributes(), ...['productTitle']]; } + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(parent::defineCardAttributes(), [ + 'product' => [ + 'label' => Craft::t('commerce', 'Product'), + ], + ]); + } + /** * @inheritdoc */ @@ -1453,15 +1398,6 @@ protected function attributeHtml(string $attribute): string return sprintf(' %s', $product->getStatus(), Html::encode($product->title)); } - if ($attribute === 'priceView') { - $price = $this->basePriceAsCurrency; - if ($this->getBasePromotionalPrice() && $this->getBasePromotionalPrice() < $this->getBasePrice()) { - $price = Html::tag('del', $price, ['style' => 'opacity: .5']) . ' ' . $this->basePromotionalPriceAsCurrency; - } - - return $price; - } - return parent::attributeHtml($attribute); } } From ae5601c4658de09d1b10d12c38421714b13110b9 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 10 Dec 2024 12:43:02 +0000 Subject: [PATCH 42/44] fix cs --- src/base/Purchasable.php | 8 ++++---- src/elements/Product.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index 2a95d0fd36..d3fc8d55c3 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -1281,7 +1281,7 @@ protected function attributeHtml(string $attribute): string $dimensions = array_filter([ $this->length, $this->width, - $this->height + $this->height, ]); } @@ -1348,9 +1348,9 @@ protected static function defineDefaultTableAttributes(string $source): array */ public static function attributePreviewHtml(array $attribute): mixed { - return match($attribute['value']) { + return match ($attribute['value']) { 'sku', 'priceView', 'dimensions', 'weight' => $attribute['placeholder'], - 'availableForPurchase', 'promotable' => Html::tag('span', '', [ + 'availableForPurchase', 'promotable' => Html::tag('span', '', [ 'class' => 'checkbox-icon', 'role' => 'img', 'title' => $attribute['label'], @@ -1415,7 +1415,7 @@ protected static function defineCardAttributes(): array 'weight' => [ 'label' => Craft::t('commerce', 'Weight'), 'placeholder' => 123 . Plugin::getInstance()->getSettings()->weightUnits, - ] + ], ]); } diff --git a/src/elements/Product.php b/src/elements/Product.php index 895920d15a..8261a18620 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -570,7 +570,7 @@ protected static function defineDefaultTableAttributes(string $source): array */ public static function attributePreviewHtml(array $attribute): mixed { - return match($attribute['value']) { + return match ($attribute['value']) { 'defaultSku' => $attribute['placeholder'], default => parent::attributePreviewHtml($attribute) }; From 225c92425027b8e5621c4cc234e00bdf6f1556d7 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 11 Dec 2024 08:16:58 +0000 Subject: [PATCH 43/44] Added product condition rule for variant conditions --- CHANGELOG-WIP.md | 9 ++- .../variants/ProductConditionRule.php | 68 +++++++++++++++++++ .../conditions/variants/VariantCondition.php | 1 + 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/elements/conditions/variants/ProductConditionRule.php diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index c9e510bf7d..055774d780 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,7 +1,12 @@ # Release Notes for Craft Commerce (WIP) -## Administration +### Store Management - Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738)) - Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430)) -- Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) +- Order conditions can now have a “Payment Gateway” rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) +- Variant conditions can now have a “Product” rule. + +### Extensibility + +- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`. \ No newline at end of file diff --git a/src/elements/conditions/variants/ProductConditionRule.php b/src/elements/conditions/variants/ProductConditionRule.php new file mode 100644 index 0000000000..aebc6c582d --- /dev/null +++ b/src/elements/conditions/variants/ProductConditionRule.php @@ -0,0 +1,68 @@ + + * @since 5.3.0 + */ +class ProductConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface +{ + /** + * @inheritdoc + */ + protected function elementType(): string + { + return Product::class; + } + + /** + * @inheritdoc + */ + public function getLabel(): string + { + return Craft::t('commerce', 'Product'); + } + + /** + * @inheritdoc + */ + public function getExclusiveQueryParams(): array + { + return ['product', 'productId', 'primaryOwnerId', 'primaryOwner', 'owner', 'ownerId']; + } + + /** + * @inheritdoc + */ + public function modifyQuery(ElementQueryInterface $query): void + { + /** @var VariantQuery $query */ + $query->ownerId($this->getElementId()); + } + + /** + * @inheritdoc + */ + public function matchElement(ElementInterface $element): bool + { + /** @var Variant $element */ + return $element->getOwnerId() == $this->getElementId(); + } +} \ No newline at end of file diff --git a/src/elements/conditions/variants/VariantCondition.php b/src/elements/conditions/variants/VariantCondition.php index 311b7baffd..49c3530b53 100644 --- a/src/elements/conditions/variants/VariantCondition.php +++ b/src/elements/conditions/variants/VariantCondition.php @@ -25,6 +25,7 @@ class VariantCondition extends ElementCondition protected function selectableConditionRules(): array { return array_merge(parent::selectableConditionRules(), [ + ProductConditionRule::class, SkuConditionRule::class, ]); } From b6c5f036354911c40034ab5a39f32c073e87f0f1 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 11 Dec 2024 08:21:20 +0000 Subject: [PATCH 44/44] fix cs --- src/elements/conditions/variants/ProductConditionRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/conditions/variants/ProductConditionRule.php b/src/elements/conditions/variants/ProductConditionRule.php index aebc6c582d..c2464ab969 100644 --- a/src/elements/conditions/variants/ProductConditionRule.php +++ b/src/elements/conditions/variants/ProductConditionRule.php @@ -65,4 +65,4 @@ public function matchElement(ElementInterface $element): bool /** @var Variant $element */ return $element->getOwnerId() == $this->getElementId(); } -} \ No newline at end of file +}