diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/create.yaml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/create.yaml
index a9b633faef..79bce3076c 100644
--- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/create.yaml
+++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/create.yaml
@@ -81,6 +81,9 @@ sylius_twig_hooks:
tax_calculation_strategy:
template: '@SyliusAdmin/channel/form/sections/money/tax_calculation_strategy.html.twig'
priority: 200
+ show_prices_including_tax:
+ template: '@SyliusAdmin/channel/form/sections/money/show_prices_including_tax.html.twig'
+ priority: 150
currencies:
template: '@SyliusAdmin/channel/form/sections/money/currencies.html.twig'
priority: 100
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/update.yaml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/update.yaml
index 99d42ccc58..c99cce7fc4 100644
--- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/update.yaml
+++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/twig_hooks/channel/update.yaml
@@ -83,6 +83,9 @@ sylius_twig_hooks:
tax_calculation_strategy:
template: '@SyliusAdmin/channel/form/sections/money/tax_calculation_strategy.html.twig'
priority: 200
+ show_prices_including_tax:
+ template: '@SyliusAdmin/channel/form/sections/money/show_prices_including_tax.html.twig'
+ priority: 150
currencies:
template: '@SyliusAdmin/channel/form/sections/money/currencies.html.twig'
priority: 100
diff --git a/src/Sylius/Bundle/AdminBundle/templates/channel/form/sections/money/show_prices_including_tax.html.twig b/src/Sylius/Bundle/AdminBundle/templates/channel/form/sections/money/show_prices_including_tax.html.twig
new file mode 100644
index 0000000000..322ae06486
--- /dev/null
+++ b/src/Sylius/Bundle/AdminBundle/templates/channel/form/sections/money/show_prices_including_tax.html.twig
@@ -0,0 +1,3 @@
+
+ {{ form_row(hookable_metadata.context.form.showPricesIncludingTax) }}
+
diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php
index e9c2eb3291..a4c656ef23 100644
--- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php
+++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php
@@ -68,6 +68,10 @@ final class ChannelTypeExtension extends AbstractTypeExtension
->add('taxCalculationStrategy', TaxCalculationStrategyChoiceType::class, [
'label' => 'sylius.form.channel.tax_calculation_strategy',
])
+ ->add('showPricesIncludingTax', CheckboxType::class, [
+ 'label' => 'sylius.form.channel.show_prices_including_tax',
+ 'required' => false,
+ ])
->add('contactEmail', EmailType::class, [
'label' => 'sylius.form.channel.contact_email',
'required' => false,
diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100000.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100000.php
new file mode 100644
index 0000000000..a8ff798a7e
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100000.php
@@ -0,0 +1,35 @@
+addSql('ALTER TABLE sylius_channel ADD show_prices_including_tax TINYINT(1) NOT NULL DEFAULT 0');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE sylius_channel DROP show_prices_including_tax');
+ }
+}
diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100001.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100001.php
new file mode 100644
index 0000000000..003e1037cb
--- /dev/null
+++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20260618100001.php
@@ -0,0 +1,35 @@
+addSql('ALTER TABLE sylius_channel ADD show_prices_including_tax BOOLEAN NOT NULL DEFAULT false');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE sylius_channel DROP COLUMN show_prices_including_tax');
+ }
+}
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Channel.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Channel.orm.xml
index 83eba4b347..61bb0c2435 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Channel.orm.xml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Channel.orm.xml
@@ -29,6 +29,11 @@
+
+
+
+
+
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml
index 6899394a2a..2abd73078d 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml
@@ -178,6 +178,16 @@
+
+
+
+
+
+
diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
index 4c97e99907..c9129f7f85 100644
--- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
+++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml
@@ -105,6 +105,7 @@ sylius:
shop_billing_data: Shop billing data
skipping_shipping_step_allowed: Skip shipping step if only one shipping method is available?
skipping_payment_step_allowed: Skip payment step if only one payment method is available?
+ show_prices_including_tax: Show prices including tax in catalog
tax_calculation_strategy: Tax calculation strategy
tax_zone_default: Default tax zone
taxonomies: Taxonomies
diff --git a/src/Sylius/Component/Core/Calculator/TaxInclusivePricesCalculator.php b/src/Sylius/Component/Core/Calculator/TaxInclusivePricesCalculator.php
new file mode 100644
index 0000000000..f8ad9510ce
--- /dev/null
+++ b/src/Sylius/Component/Core/Calculator/TaxInclusivePricesCalculator.php
@@ -0,0 +1,80 @@
+applyTax(
+ $this->inner->calculate($productVariant, $context),
+ $productVariant,
+ $context,
+ );
+ }
+
+ public function calculateOriginal(ProductVariantInterface $productVariant, array $context): int
+ {
+ return $this->applyTax(
+ $this->inner->calculateOriginal($productVariant, $context),
+ $productVariant,
+ $context,
+ );
+ }
+
+ public function calculateLowestPriceBeforeDiscount(ProductVariantInterface $productVariant, array $context): ?int
+ {
+ $price = $this->inner->calculateLowestPriceBeforeDiscount($productVariant, $context);
+ if (null === $price) {
+ return null;
+ }
+
+ return $this->applyTax($price, $productVariant, $context);
+ }
+
+ /** @param array $context */
+ private function applyTax(int $price, ProductVariantInterface $productVariant, array $context): int
+ {
+ /** @var ChannelInterface $channel */
+ $channel = $context['channel'];
+
+ if (!$channel->isShowPricesIncludingTax()) {
+ return $price;
+ }
+
+ $zone = $channel->getDefaultTaxZone();
+ if (null === $zone) {
+ return $price;
+ }
+
+ $taxRate = $this->taxRateResolver->resolve($productVariant, ['zone' => $zone]);
+ if (null === $taxRate || $taxRate->isIncludedInPrice()) {
+ return $price;
+ }
+
+ return $price + (int) round($this->taxCalculator->calculate((float) $price, $taxRate));
+ }
+}
diff --git a/src/Sylius/Component/Core/Model/Channel.php b/src/Sylius/Component/Core/Model/Channel.php
index 00fa43bb7b..fe09f95f82 100644
--- a/src/Sylius/Component/Core/Model/Channel.php
+++ b/src/Sylius/Component/Core/Model/Channel.php
@@ -73,6 +73,8 @@ class Channel extends BaseChannel implements ChannelInterface
protected ?ChannelPriceHistoryConfigInterface $channelPriceHistoryConfig = null;
+ protected bool $showPricesIncludingTax = false;
+
public function __construct()
{
parent::__construct();
@@ -293,6 +295,16 @@ class Channel extends BaseChannel implements ChannelInterface
$this->channelPriceHistoryConfig = $channelPriceHistoryConfig;
}
+ public function isShowPricesIncludingTax(): bool
+ {
+ return $this->showPricesIncludingTax;
+ }
+
+ public function setShowPricesIncludingTax(bool $showPricesIncludingTax): void
+ {
+ $this->showPricesIncludingTax = $showPricesIncludingTax;
+ }
+
public function getEnabledCountries(): Collection
{
return $this->getCountries()->filter(fn (CountryInterface $country): bool => true === $country->isEnabled());
diff --git a/src/Sylius/Component/Core/Model/ChannelInterface.php b/src/Sylius/Component/Core/Model/ChannelInterface.php
index 8f6e34f138..52c641c16a 100644
--- a/src/Sylius/Component/Core/Model/ChannelInterface.php
+++ b/src/Sylius/Component/Core/Model/ChannelInterface.php
@@ -91,6 +91,10 @@ interface ChannelInterface extends
public function hasCountry(CountryInterface $country): bool;
+ public function isShowPricesIncludingTax(): bool;
+
+ public function setShowPricesIncludingTax(bool $showPricesIncludingTax): void;
+
public function setChannelPriceHistoryConfig(ChannelPriceHistoryConfigInterface $channelPriceHistoryConfig): void;
public function getChannelPriceHistoryConfig(): ?ChannelPriceHistoryConfigInterface;
diff --git a/src/Sylius/Component/Core/tests/Calculator/TaxInclusivePricesCalculatorTest.php b/src/Sylius/Component/Core/tests/Calculator/TaxInclusivePricesCalculatorTest.php
new file mode 100644
index 0000000000..ee6fe054d6
--- /dev/null
+++ b/src/Sylius/Component/Core/tests/Calculator/TaxInclusivePricesCalculatorTest.php
@@ -0,0 +1,175 @@
+inner = $this->createMock(ProductVariantPricesCalculatorInterface::class);
+ $this->taxRateResolver = $this->createMock(TaxRateResolverInterface::class);
+ $this->taxCalculator = $this->createMock(CalculatorInterface::class);
+ $this->productVariant = $this->createMock(ProductVariantInterface::class);
+ $this->channel = $this->createMock(ChannelInterface::class);
+ $this->calculator = new TaxInclusivePricesCalculator(
+ $this->inner,
+ $this->taxRateResolver,
+ $this->taxCalculator,
+ );
+ }
+
+ public function testItImplementsInterface(): void
+ {
+ $this->assertInstanceOf(ProductVariantPricesCalculatorInterface::class, $this->calculator);
+ }
+
+ public function testItReturnsPriceUnchangedWhenFlagIsDisabled(): void
+ {
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(false);
+ $this->inner->method('calculate')->willReturn(10000);
+ $this->taxRateResolver->expects($this->never())->method('resolve');
+
+ $result = $this->calculator->calculate($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(10000, $result);
+ }
+
+ public function testItReturnsPriceUnchangedWhenNoDefaultTaxZone(): void
+ {
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn(null);
+ $this->inner->method('calculate')->willReturn(10000);
+ $this->taxRateResolver->expects($this->never())->method('resolve');
+
+ $result = $this->calculator->calculate($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(10000, $result);
+ }
+
+ public function testItReturnsPriceUnchangedWhenNoTaxRateResolved(): void
+ {
+ $zone = $this->createMock(ZoneInterface::class);
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn($zone);
+ $this->inner->method('calculate')->willReturn(10000);
+ $this->taxRateResolver->method('resolve')->willReturn(null);
+
+ $result = $this->calculator->calculate($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(10000, $result);
+ }
+
+ public function testItReturnsPriceUnchangedWhenTaxRateIsIncludedInPrice(): void
+ {
+ $zone = $this->createMock(ZoneInterface::class);
+ $taxRate = $this->createMock(TaxRateInterface::class);
+ $taxRate->method('isIncludedInPrice')->willReturn(true);
+
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn($zone);
+ $this->inner->method('calculate')->willReturn(10000);
+ $this->taxRateResolver->method('resolve')->willReturn($taxRate);
+ $this->taxCalculator->expects($this->never())->method('calculate');
+
+ $result = $this->calculator->calculate($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(10000, $result);
+ }
+
+ public function testItAddsTaxToPriceWhenFlagIsEnabled(): void
+ {
+ $zone = $this->createMock(ZoneInterface::class);
+ $taxRate = $this->createMock(TaxRateInterface::class);
+ $taxRate->method('isIncludedInPrice')->willReturn(false);
+
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn($zone);
+ $this->inner->method('calculate')->willReturn(10000);
+ $this->taxRateResolver->method('resolve')->willReturn($taxRate);
+ $this->taxCalculator->method('calculate')->with(10000.0, $taxRate)->willReturn(2300.0);
+
+ $result = $this->calculator->calculate($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(12300, $result);
+ }
+
+ public function testItAddsTaxToOriginalPrice(): void
+ {
+ $zone = $this->createMock(ZoneInterface::class);
+ $taxRate = $this->createMock(TaxRateInterface::class);
+ $taxRate->method('isIncludedInPrice')->willReturn(false);
+
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn($zone);
+ $this->inner->method('calculateOriginal')->willReturn(12000);
+ $this->taxRateResolver->method('resolve')->willReturn($taxRate);
+ $this->taxCalculator->method('calculate')->with(12000.0, $taxRate)->willReturn(2760.0);
+
+ $result = $this->calculator->calculateOriginal($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(14760, $result);
+ }
+
+ public function testItReturnsNullForLowestPriceWhenInnerReturnsNull(): void
+ {
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->inner->method('calculateLowestPriceBeforeDiscount')->willReturn(null);
+
+ $result = $this->calculator->calculateLowestPriceBeforeDiscount($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertNull($result);
+ }
+
+ public function testItAddsTaxToLowestPrice(): void
+ {
+ $zone = $this->createMock(ZoneInterface::class);
+ $taxRate = $this->createMock(TaxRateInterface::class);
+ $taxRate->method('isIncludedInPrice')->willReturn(false);
+
+ $this->channel->method('isShowPricesIncludingTax')->willReturn(true);
+ $this->channel->method('getDefaultTaxZone')->willReturn($zone);
+ $this->inner->method('calculateLowestPriceBeforeDiscount')->willReturn(9000);
+ $this->taxRateResolver->method('resolve')->willReturn($taxRate);
+ $this->taxCalculator->method('calculate')->with(9000.0, $taxRate)->willReturn(2070.0);
+
+ $result = $this->calculator->calculateLowestPriceBeforeDiscount($this->productVariant, ['channel' => $this->channel]);
+
+ $this->assertSame(11070, $result);
+ }
+}