mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-05 20:57:12 +00:00
Merge 5499b7a213 into cbb32838fb
This commit is contained in:
commit
0793ccca9e
13 changed files with 370 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<div class="col-12">
|
||||
{{ form_row(hookable_metadata.context.form.showPricesIncludingTax) }}
|
||||
</div>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\CoreBundle\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260618100000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add show_prices_including_tax column to sylius_channel table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\CoreBundle\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractPostgreSQLMigration;
|
||||
|
||||
final class Version20260618100001 extends AbstractPostgreSQLMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add show_prices_including_tax column to sylius_channel table (PostgreSQL)';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,11 @@
|
|||
<option name="default">0</option>
|
||||
</options>
|
||||
</field>
|
||||
<field name="showPricesIncludingTax" column="show_prices_including_tax" type="boolean">
|
||||
<options>
|
||||
<option name="default">0</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<many-to-one field="defaultLocale" target-entity="Sylius\Component\Locale\Model\LocaleInterface" fetch="EAGER">
|
||||
<join-column name="default_locale_id" referenced-column-name="id" nullable="false" />
|
||||
|
|
|
|||
|
|
@ -178,6 +178,16 @@
|
|||
</service>
|
||||
<service id="Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface" alias="sylius.calculator.product_variant_price" />
|
||||
|
||||
<service
|
||||
id="sylius.calculator.product_variant_price.tax_inclusive"
|
||||
class="Sylius\Component\Core\Calculator\TaxInclusivePricesCalculator"
|
||||
decorates="sylius.calculator.product_variant_price"
|
||||
>
|
||||
<argument type="service" id="sylius.calculator.product_variant_price.tax_inclusive.inner" />
|
||||
<argument type="service" id="sylius.resolver.tax_rate" />
|
||||
<argument type="service" id="sylius.tax_calculator" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.section_resolver.uri_based" class="Sylius\Bundle\CoreBundle\SectionResolver\UriBasedSectionProvider">
|
||||
<argument type="service" id="request_stack" />
|
||||
<argument type="collection" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Component\Core\Calculator;
|
||||
|
||||
use Sylius\Component\Core\Model\ChannelInterface;
|
||||
use Sylius\Component\Core\Model\ProductVariantInterface;
|
||||
use Sylius\Component\Taxation\Calculator\CalculatorInterface;
|
||||
use Sylius\Component\Taxation\Resolver\TaxRateResolverInterface;
|
||||
|
||||
final class TaxInclusivePricesCalculator implements ProductVariantPricesCalculatorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductVariantPricesCalculatorInterface $inner,
|
||||
private readonly TaxRateResolverInterface $taxRateResolver,
|
||||
private readonly CalculatorInterface $taxCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function calculate(ProductVariantInterface $productVariant, array $context): int
|
||||
{
|
||||
return $this->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<string, mixed> $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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Sylius\Component\Core\Calculator;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Component\Addressing\Model\ZoneInterface;
|
||||
use Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface;
|
||||
use Sylius\Component\Core\Calculator\TaxInclusivePricesCalculator;
|
||||
use Sylius\Component\Core\Model\ChannelInterface;
|
||||
use Sylius\Component\Core\Model\ProductVariantInterface;
|
||||
use Sylius\Component\Taxation\Calculator\CalculatorInterface;
|
||||
use Sylius\Component\Taxation\Model\TaxRateInterface;
|
||||
use Sylius\Component\Taxation\Resolver\TaxRateResolverInterface;
|
||||
|
||||
#[CoversClass(TaxInclusivePricesCalculator::class)]
|
||||
final class TaxInclusivePricesCalculatorTest extends TestCase
|
||||
{
|
||||
private MockObject&ProductVariantPricesCalculatorInterface $inner;
|
||||
|
||||
private MockObject&TaxRateResolverInterface $taxRateResolver;
|
||||
|
||||
private CalculatorInterface&MockObject $taxCalculator;
|
||||
|
||||
private MockObject&ProductVariantInterface $productVariant;
|
||||
|
||||
private ChannelInterface&MockObject $channel;
|
||||
|
||||
private TaxInclusivePricesCalculator $calculator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue