diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index b52dddf1c9..4aa80db044 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,4 +1,4 @@ -# WIP Release Notes for Craft Commerce +# Release Notes for Craft Commerce 5.5 (WIP) ### Store Management - It is now possible to suppress order emails when marking an order as complete in the control panel. ([#4144](https://github.com/craftcms/commerce/issues/4144)) @@ -23,6 +23,9 @@ - Added `craft\commerce\events\InventoryMovementEvent`. ([#4063](https://github.com/craftcms/commerce/pull/4063)) - Added `craft\commerce\events\UpdateInventoryLevelEvent`. ([#4063](https://github.com/craftcms/commerce/pull/4063)) - Added `craft\commerce\helpers\Gql::getSchemaContainedProductTypes()`. +- Added `craft\commerce\models\Email::$renderSiteId`. +- Added `craft\commerce\models\Email::getRenderSite()`. +- Added `craft\commerce\records\Email::$renderSiteId`. - Added `craft\commerce\records\Order::$dateFirstPaid`. - Added `craft\commerce\services\CatalogPricingRules::hasCatalogPricingRules()`. - Added `craft\commerce\services\Discounts::appendCouponCode()`. ([#4084](https://github.com/craftcms/commerce/pull/4084)) @@ -31,3 +34,4 @@ ### System - Fixed a bug where purchasables could have a shipping category that was no longer available to their product type. ([#4018](https://github.com/craftcms/commerce/issues/4018)) +- Fixed a bug where order emails weren’t always getting rendered for the correct site. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index af7b8a1161..100740d796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ - Improved store query performance. ([#4029](https://github.com/craftcms/commerce/issues/4029)) - Fixed a bug where the purchasable cache was not cleared when stock was updated. - Fixed a PHP error that could occur when sending emails. ([#4017](https://github.com/craftcms/commerce/issues/4017)) +- Fixed a bug where order emails weren’t always getting rendered for the correct site. - Fixed a SQL error that could occur when upgrading to Commerce 5. ([#4044](https://github.com/craftcms/commerce/issues/4044)) - Fixed a bug where duplicate order references could be generated. ([#4050](https://github.com/craftcms/commerce/issues/4050)) - Fixed a bug where purchasables’ `shippingCategoryId` and `taxCategoryId` properties couldn’t be set via `setAttributes()`. ([#4046](https://github.com/craftcms/commerce/issues/4046)) diff --git a/src/controllers/EmailsController.php b/src/controllers/EmailsController.php index 2632b4523d..488238eca0 100644 --- a/src/controllers/EmailsController.php +++ b/src/controllers/EmailsController.php @@ -17,6 +17,7 @@ use craft\commerce\records\Email as EmailRecord; use craft\helpers\App; use craft\helpers\ArrayHelper; +use craft\models\Site; use yii\base\ErrorException; use yii\base\Exception; use yii\base\InvalidConfigException; @@ -103,6 +104,11 @@ public function actionEdit(?string $storeHandle = null, int $id = null, Email $e $variables['emailLanguageOptions'] = array_merge($emailLanguageOptions, LocaleHelper::getSiteAndOtherLanguages()); + $variables['emailRenderSiteOptions'] = [ + null => Craft::t('commerce', 'The site the order was made in.'), + ['optgroup' => Craft::t('commerce', 'Sites')], + ] + collect(Craft::$app->getSites()->getAllSites())->mapWithKeys(fn(Site $site) => [$site->id => $site->name])->all(); + $variables['readOnly'] = $this->isReadOnlyScreen(); return $this->renderTemplate('commerce/settings/emails/_edit', $variables); @@ -136,6 +142,8 @@ public function actionSave(): ?Response $email = new Email(); } + $renderSiteId = $this->request->getBodyParam('renderSiteId'); + // Shared attributes $email->storeId = $storeId; $email->name = $this->request->getBodyParam('name'); @@ -151,6 +159,7 @@ public function actionSave(): ?Response $pdfId = $this->request->getBodyParam('pdfId'); $email->pdfId = $pdfId ?: null; $email->language = $this->request->getBodyParam('language'); + $email->renderSiteId = $renderSiteId ? (int)$renderSiteId : null; $email->setSenderAddress($this->request->getBodyParam('senderAddress')); $email->setSenderName($this->request->getBodyParam('senderName')); diff --git a/src/migrations/Install.php b/src/migrations/Install.php index a03b5c7fc2..da86d8d9ae 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -271,6 +271,7 @@ public function createTables(): void 'plainTextTemplatePath' => $this->string(), 'pdfId' => $this->integer(), 'language' => $this->string(), + 'renderSiteId' => $this->integer(), 'dateCreated' => $this->dateTime()->notNull(), 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), @@ -1216,6 +1217,7 @@ public function addForeignKeys(): void $this->addForeignKey(null, Table::DONATIONS, ['id'], '{{%elements}}', ['id'], 'CASCADE'); $this->addForeignKey(null, Table::EMAILS, ['pdfId'], Table::PDFS, ['id'], 'SET NULL'); $this->addForeignKey(null, Table::EMAILS, ['storeId'], Table::STORES, ['id'], 'CASCADE', 'CASCADE'); + $this->addForeignKey(null, Table::EMAILS, ['renderSiteId'], CraftTable::SITES, ['id'], 'SET NULL'); $this->addForeignKey(null, Table::EMAIL_DISCOUNTUSES, ['discountId'], Table::DISCOUNTS, ['id'], 'CASCADE', 'CASCADE'); $this->addForeignKey(null, Table::INVENTORYITEMS, 'purchasableId', Table::PURCHASABLES, 'id', 'CASCADE', null); $this->addForeignKey(null, Table::INVENTORYLOCATIONS, 'addressId', CraftTable::ELEMENTS, 'id', 'CASCADE', null); diff --git a/src/migrations/m250617_105249_add_email_render_site_id.php b/src/migrations/m250617_105249_add_email_render_site_id.php new file mode 100644 index 0000000000..b6ea5f1756 --- /dev/null +++ b/src/migrations/m250617_105249_add_email_render_site_id.php @@ -0,0 +1,64 @@ +db->columnExists(Table::EMAILS, 'renderSiteId')) { + return true; + } + + $this->addColumn(Table::EMAILS, 'renderSiteId', $this->integer()->after('language')); + + // Get the primary site ID + $primarySite = Craft::$app->getSites()->getPrimarySite(); + + // For all current emails set the `renderSiteId` to the primary site to keep the existing behavior + $this->db->createCommand()->update( + Table::EMAILS, + ['renderSiteId' => $primarySite->id], + ['renderSiteId' => null] + )->execute(); + + // Update the project config + $projectConfig = Craft::$app->getProjectConfig(); + + $emails = $projectConfig->get('commerce.emails') ?? []; + $muteEvents = $projectConfig->muteEvents; + $projectConfig->muteEvents = true; + + foreach ($emails as $emailUid => $email) { + $email['renderSite'] = $primarySite->uid; + $projectConfig->set("commerce.emails.$emailUid", $email); + } + + $projectConfig->muteEvents = $muteEvents; + + // Add foreign key + $this->addForeignKey(null, Table::EMAILS, ['renderSiteId'], CraftTable::SITES, ['id'], 'SET NULL', 'CASCADE'); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m250617_105249_add_email_render_site_id cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/Email.php b/src/models/Email.php index 7fa88f5fd4..a13499a313 100644 --- a/src/models/Email.php +++ b/src/models/Email.php @@ -7,14 +7,17 @@ namespace craft\commerce\models; +use Craft; use craft\commerce\base\HasStoreInterface; use craft\commerce\base\Model; use craft\commerce\base\StoreTrait; use craft\commerce\elements\Order; use craft\commerce\Plugin; use craft\commerce\records\Email as EmailRecord; +use craft\errors\SiteNotFoundException; use craft\helpers\App; use craft\helpers\UrlHelper; +use craft\models\Site; use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; @@ -86,6 +89,14 @@ class Email extends Model implements HasStoreInterface */ public string $language = EmailRecord::LOCALE_ORDER_LANGUAGE; + /** + * The site the email should be rendered in. Set to `null` to use the site the order was placed in. + * + * @var int|null + * @since 5.5.0 + */ + public ?int $renderSiteId = null; + /** * @var string|null * @since 5.0.0 @@ -144,7 +155,7 @@ public function extraFields(): array } /** - * Determines the language this pdf, if + * Determines the language this email is rendered in. * * @param Order|null $order */ @@ -163,6 +174,25 @@ public function getRenderLanguage(Order $order = null): string return $language; } + /** + * Determines the site this email is rendered in. + * + * @param Order|null $order + * @return Site + * @throws SiteNotFoundException + * @since 5.5.0 + */ + public function getRenderSite(Order $order = null): Site + { + $renderSiteId = $this->renderSiteId ?? $order?->orderSiteId; + + if ($renderSiteId !== null) { + return Craft::$app->getSites()->getSiteById($renderSiteId); + } + + return Craft::$app->getSites()->getPrimarySite(); + } + /** * @inheritdoc */ @@ -187,6 +217,7 @@ protected function defineRules(): array 'pdfId', 'plainTextTemplatePath', 'recipientType', + 'renderSiteId', 'replyTo', 'senderAddress', 'senderName', @@ -368,6 +399,7 @@ public function getConfig(): array 'pdf' => $this->getPdf()?->uid, 'plainTextTemplatePath' => $this->plainTextTemplatePath ?? null, 'recipientType' => $this->recipientType, + 'renderSite' => $this->renderSiteId ? Craft::$app->getSites()->getSiteById($this->renderSiteId)?->uid ?? null : null, 'replyTo' => $this->replyTo ?: null, 'store' => $this->getStore()->uid, 'subject' => $this->subject, diff --git a/src/records/Email.php b/src/records/Email.php index bdc113d1a1..0eca86e947 100644 --- a/src/records/Email.php +++ b/src/records/Email.php @@ -21,6 +21,7 @@ * @property int $id * @property string $name * @property string $recipientType + * @property int $renderSiteId * @property string $senderAddress * @property string $senderName * @property string $subject diff --git a/src/services/Emails.php b/src/services/Emails.php index 05c557e752..4b000c0e65 100644 --- a/src/services/Emails.php +++ b/src/services/Emails.php @@ -341,6 +341,7 @@ public function handleChangedEmail(ConfigEvent $event): void $emailRecord = $this->_getEmailRecord($emailUid); $isNewEmail = $emailRecord->getIsNewRecord(); $store = Plugin::getInstance()->getStores()->getStoreByUid($data['store']); + $renderSite = array_key_exists('renderSite', $data) && $data['renderSite'] !== null ? Craft::$app->getSites()->getSiteByUid($data['renderSite']) : null; $emailRecord->storeId = $store->id; $emailRecord->name = $data['name']; @@ -358,6 +359,7 @@ public function handleChangedEmail(ConfigEvent $event): void $emailRecord->uid = $emailUid; $emailRecord->pdfId = $pdfUid ? Db::idByUid(Table::PDFS, $pdfUid) : null; $emailRecord->language = $data['language'] ?? EmailRecord::LOCALE_ORDER_LANGUAGE; + $emailRecord->renderSiteId = $renderSite?->id ?? null; $emailRecord->save(false); @@ -477,6 +479,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor $originalLanguage = Craft::$app->language; $originalFormattingLanguage = Craft::$app->formattingLocale; $emailLanguage = $email->getRenderLanguage($order); + $emailSite = $email->getRenderSite($order); Locale::switchAppLanguage($emailLanguage); @@ -783,6 +786,9 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor } } + $originalSiteId = Craft::$app->getSites()->getCurrentSite()->id; + Craft::$app->getSites()->setCurrentSite($emailSite); + // Render HTML body try { $body = $view->renderTemplate($templatePath, $renderVariables); @@ -799,6 +805,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor ]); Craft::error($error, __METHOD__); + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -823,6 +830,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor ]); Craft::error($error, __METHOD__); + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -850,6 +858,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor Craft::info($notice, __METHOD__); + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -869,6 +878,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor Craft::error($error, __METHOD__); + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -888,6 +898,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor Craft::error($error, __METHOD__); + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -906,6 +917,7 @@ public function sendEmail(Email $email, Order $order, ?OrderHistory $orderHistor ])); } + Craft::$app->getSites()->setCurrentSite($originalSiteId); Locale::switchAppLanguage($originalLanguage, $originalFormattingLanguage->id); $view->setTemplateMode($oldTemplateMode); $generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad; @@ -957,6 +969,7 @@ private function _createEmailQuery(): Query 'emails.pdfId', 'emails.plainTextTemplatePath', 'emails.recipientType', + 'emails.renderSiteId', 'emails.replyTo', 'emails.senderAddress', 'emails.senderName', diff --git a/src/templates/settings/emails/_edit.twig b/src/templates/settings/emails/_edit.twig index 2dacd09122..13a2188c30 100644 --- a/src/templates/settings/emails/_edit.twig +++ b/src/templates/settings/emails/_edit.twig @@ -182,7 +182,7 @@ {{ forms.selectField({ label: 'Language'|t('commerce'), - instructions: "The language to be used when email is rendered."|t, + instructions: "The language to be used when email is rendered."|t('commerce'), id: 'language', name: 'language', options : emailLanguageOptions, @@ -192,6 +192,17 @@ disabled: readOnly, }) }} + {{ forms.selectField({ + label: 'Site'|t('app'), + instructions: "The site to be used when email is rendered."|t('commerce'), + id: 'renderSiteId', + name: 'renderSiteId', + options : emailRenderSiteOptions, + value : email.renderSiteId, + errors: email.getErrors('renderSiteId') ?? null, + disabled: readOnly, + }) }} + {{ forms.lightswitchField({ label: "Enabled?"|t('commerce'), instructions: 'If disabled, this email will not send.'|t('commerce'), diff --git a/src/translations/en/commerce.php b/src/translations/en/commerce.php index e04dc409c8..53fd79ca16 100644 --- a/src/translations/en/commerce.php +++ b/src/translations/en/commerce.php @@ -1140,6 +1140,8 @@ 'The purchasable is related by another element' => 'The purchasable is related by another element', 'The recipient of the email. Twig code can be used here.' => 'The recipient of the email. Twig code can be used here.', 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.' => 'The reply to email address. Leave blank for normal reply to of email sender. Twig code can be used here.', + 'The site the order was made in.' => 'The site the order was made in.', + 'The site to be used when email is rendered.' => 'The site to be used when email is rendered.', 'The subject line of the email. Twig code can be used here.' => 'The subject line of the email. Twig code can be used here.', 'The template that the PDF should be generated from.' => 'The template that the PDF should be generated from.', 'The template to be used for HTML emails.' => 'The template to be used for HTML emails.',