From 3bff5fa8bd313889c7a52243e86e6fc379aeae11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Feb 2026 21:54:34 +0100 Subject: [PATCH 1/3] Allow to set if prices contain VAT or not in orderdetail --- .../elements/collection_type_controller.js | 12 ++- assets/js/tristate_checkboxes.js | 3 +- src/Entity/PriceInformations/Orderdetail.php | 45 ++++++++++++ src/Entity/PriceInformations/Pricedetail.php | 32 ++++---- src/Form/Part/OrderdetailType.php | 6 ++ .../parts/edit/edit_form_styles.html.twig | 3 +- tests/Entity/PriceSystem/OrderdetailTest.php | 73 +++++++++++++++++++ translations/messages.en.xlf | 6 ++ 8 files changed, 159 insertions(+), 21 deletions(-) diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 14b683e0..67022ef2 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -74,16 +74,24 @@ export default class extends Controller { const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID())); + let ret = null; + //Insert new html after the last child element //If the table has a tbody, insert it there //Afterwards return the newly created row if(targetTable.tBodies[0]) { targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr); - return targetTable.tBodies[0].lastElementChild; + ret = targetTable.tBodies[0].lastElementChild; } else { //Otherwise just insert it targetTable.insertAdjacentHTML('beforeend', newElementStr); - return targetTable.lastElementChild; + ret = targetTable.lastElementChild; } + + //Trigger an event to notify other components that a new element has been created, so they can for example initialize select2 on it + targetTable.dispatchEvent(new CustomEvent("collection:elementAdded", {bubbles: true})); + + return ret; + } /** diff --git a/assets/js/tristate_checkboxes.js b/assets/js/tristate_checkboxes.js index 4cf4fc1e..467099ab 100644 --- a/assets/js/tristate_checkboxes.js +++ b/assets/js/tristate_checkboxes.js @@ -56,7 +56,8 @@ class TristateHelper { document.addEventListener("turbo:load", listener); document.addEventListener("turbo:render", listener); + document.addEventListener("collection:elementAdded", listener); } } -export default new TristateHelper(); \ No newline at end of file +export default new TristateHelper(); diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 8ed76a46..9a9a2823 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -52,6 +52,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; @@ -388,6 +389,50 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this; } + /** + * Checks if the prices of this orderdetail include VAT. This is determined by checking the pricedetails of this + * orderdetail. If there are no pricedetails or if the pricedetails have conflicting values, null is returned. + * @return bool|null + */ + #[Groups(['orderdetail:read'])] + #[SerializedName('prices_include_vat')] + public function getPricesIncludesVAT(): ?bool + { + $value = null; + //We determine that via the pricedetails + foreach ($this->getPricedetails() as $pricedetail) { + /** @var Pricedetail $pricedetail */ + if ($pricedetail->getIncludesVat() === null) { + return null; // If any pricedetail doesn't specify this, we can't determine it + } + + if ($value === null) { + $value = $pricedetail->getIncludesVat(); // Set initial value + } elseif ($value !== $pricedetail->getIncludesVat()) { + return null; // If there are conflicting values, we can't determine it + } + } + + return $value; + } + + /** + * Sets whether the prices of this orderdetail include VAT. This is set for all pricedetails of this orderdetail. + * @param bool|null $includesVat + * @return $this + */ + #[Groups(['orderdetail:write'])] + #[SerializedName('prices_include_vat')] + public function setPricesIncludesVAT(?bool $includesVat): self + { + foreach ($this->getPricedetails() as $pricedetail) { + /** @var Pricedetail $pricedetail */ + $pricedetail->setIncludesVat($includesVat); + } + + return $this; + } + public function getName(): string { return $this->getSupplierPartNr(); diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index b98ee056..7deb64f9 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -124,9 +124,9 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface /** * @var bool|null Whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. */ - #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + #[ORM\Column(name: "include_vat", type: Types::BOOLEAN, nullable: true)] #[Groups(['extended', 'full', 'import', 'pricedetail:read', 'pricedetail:write'])] - protected ?bool $include_vat = null; + protected ?bool $includes_vat = null; public function __construct() { @@ -271,6 +271,15 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface return $this->currency?->getIsoCode(); } + /** + * Returns whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. + * @return bool|null + */ + public function getIncludesVat(): ?bool + { + return $this->includes_vat; + } + /******************************************************************************** * * Setters @@ -369,24 +378,13 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface } /** - * Returns whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. - * @return bool|null - */ - public function getIncludeVat(): ?bool - { - return $this->include_vat; - } - - /** - * Sets whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. - * @param bool|null $include_vat + * Set whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. + * @param bool|null $includes_vat * @return $this */ - public function setIncludeVat(?bool $include_vat): self + public function setIncludesVat(?bool $includes_vat): self { - $this->include_vat = $include_vat; + $this->includes_vat = $includes_vat; return $this; } - - } diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php index 53240821..ca295c7e 100644 --- a/src/Form/Part/OrderdetailType.php +++ b/src/Form/Part/OrderdetailType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\Part; +use App\Form\Type\TriStateCheckboxType; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Supplier; @@ -73,6 +74,11 @@ class OrderdetailType extends AbstractType 'label' => 'orderdetails.edit.obsolete', ]); + $builder->add('pricesIncludesVAT', TriStateCheckboxType::class, [ + 'required' => false, + 'label' => 'orderdetails.edit.prices_includes_vat', + ]); + //Add pricedetails after we know the data, so we can set the default currency $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options): void { /** @var Orderdetail $orderdetail */ diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index c2a89b6a..aa68f38a 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -32,6 +32,7 @@ {{ form_row(form.supplierpartnr, {'attr': {'class': 'form-control-sm'}}) }} {{ form_row(form.supplier_product_url, {'attr': {'class': 'form-control-sm'}}) }} {{ form_widget(form.obsolete) }} + {{ form_row(form.pricesIncludesVAT) }}
@@ -226,4 +227,4 @@ {{ form_errors(form) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/Entity/PriceSystem/OrderdetailTest.php b/tests/Entity/PriceSystem/OrderdetailTest.php index 497f9ab3..7eae93aa 100644 --- a/tests/Entity/PriceSystem/OrderdetailTest.php +++ b/tests/Entity/PriceSystem/OrderdetailTest.php @@ -61,4 +61,77 @@ class OrderdetailTest extends TestCase $this->assertSame($price5, $orderdetail->findPriceForQty(5.3)); $this->assertSame($price5, $orderdetail->findPriceForQty(10000)); } + + public function testGetPricesIncludesVAT(): void + { + $orderdetail = new Orderdetail(); + + //By default, the pricesIncludesVAT property should be null for empty orderdetails + $this->assertNull($orderdetail->getPricesIncludesVAT()); + + $price0 = (new Pricedetail())->setMinDiscountQuantity(0.23); + $price1 = (new Pricedetail())->setMinDiscountQuantity(1); + $price5 = (new Pricedetail())->setMinDiscountQuantity(5.3); + + $orderdetail->addPricedetail($price0)->addPricedetail($price1)->addPricedetail($price5); + + //With empty pricedetails, the pricesIncludesVAT property should still be null + $this->assertNull($orderdetail->getPricesIncludesVAT()); + + //If all of the pricedetails have the same value for includesVAT, the pricesIncludesVAT property should return this value + $price0->setIncludesVAT(true); + $price1->setIncludesVAT(true); + $price5->setIncludesVAT(true); + $this->assertTrue($orderdetail->getPricesIncludesVAT()); + + $price0->setIncludesVAT(false); + $price1->setIncludesVAT(false); + $price5->setIncludesVAT(false); + $this->assertFalse($orderdetail->getPricesIncludesVAT()); + + //If the pricedetails have different values for includesVAT, the pricesIncludesVAT property should return null + $price0->setIncludesVAT(true); + $price1->setIncludesVAT(false); + $price5->setIncludesVAT(true); + $this->assertNull($orderdetail->getPricesIncludesVAT()); + + //If the pricedetails have different values for includesVAT, the pricesIncludesVAT property should return null, even if one of them is null + $price0->setIncludesVAT(null); + $price1->setIncludesVAT(false); + $price5->setIncludesVAT(false); + $this->assertNull($orderdetail->getPricesIncludesVAT()); + } + + public function testSetPricesIncludesVAT(): void + { + $orderdetail = new Orderdetail(); + $price0 = (new Pricedetail())->setMinDiscountQuantity(0.23); + $price1 = (new Pricedetail())->setMinDiscountQuantity(1); + $price5 = (new Pricedetail())->setMinDiscountQuantity(5.3); + + $orderdetail->addPricedetail($price0)->addPricedetail($price1)->addPricedetail($price5); + + $this->assertNull($orderdetail->getPricesIncludesVAT()); + + $orderdetail->setPricesIncludesVAT(true); + $this->assertTrue($orderdetail->getPricesIncludesVAT()); + //Ensure that the pricesIncludesVAT property is correctly propagated to the pricedetails + foreach ($orderdetail->getPricedetails() as $pricedetail) { + $this->assertTrue($pricedetail->getIncludesVAT()); + } + + $orderdetail->setPricesIncludesVAT(false); + $this->assertFalse($orderdetail->getPricesIncludesVAT()); + //Ensure that the pricesIncludesVAT property is correctly propagated to the pricedetails + foreach ($orderdetail->getPricedetails() as $pricedetail) { + $this->assertFalse($pricedetail->getIncludesVAT()); + } + + $orderdetail->setPricesIncludesVAT(null); + $this->assertNull($orderdetail->getPricesIncludesVAT()); + //Ensure that the pricesIncludesVAT property is correctly propagated to the pricedetails + foreach ($orderdetail->getPricedetails() as $pricedetail) { + $this->assertNull($pricedetail->getIncludesVAT()); + } + } } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f47c4e9e..2310286c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12437,5 +12437,11 @@ Buerklin-API Authentication server: Make this attachment type only available for certain element classes. Leave empty to show this attachment type for all element classes. + + + orderdetails.edit.prices_includes_vat + Prices include VAT + + From 5a47b15c97922385efa28342920888fa539ce64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Feb 2026 21:58:14 +0100 Subject: [PATCH 2/3] Use the information from info provider whether prices includes VAT or not --- assets/controllers/elements/collection_type_controller.js | 2 +- src/Services/InfoProviderSystem/DTOtoEntityConverter.php | 3 ++- tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 67022ef2..048600a9 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -86,7 +86,7 @@ export default class extends Controller { targetTable.insertAdjacentHTML('beforeend', newElementStr); ret = targetTable.lastElementChild; } - + //Trigger an event to notify other components that a new element has been created, so they can for example initialize select2 on it targetTable.dispatchEvent(new CustomEvent("collection:elementAdded", {bubbles: true})); diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index d3a7c52c..1a93b111 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -94,13 +94,14 @@ final class DTOtoEntityConverter $entity->setPrice($dto->getPriceAsBigDecimal()); $entity->setPriceRelatedQuantity($dto->price_related_quantity); - //Currency TODO if ($dto->currency_iso_code !== null) { $entity->setCurrency($this->getCurrency($dto->currency_iso_code)); } else { $entity->setCurrency(null); } + $entity->setIncludesVat($dto->includes_tax); + return $entity; } diff --git a/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php index 78e79167..54878bbf 100644 --- a/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php +++ b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php @@ -101,6 +101,8 @@ class DTOtoEntityConverterTest extends WebTestCase //For base currencies, the currency field is null $this->assertNull($entity->getCurrency()); + + $this->assertTrue($entity->getIncludesVat()); } public function testConvertPurchaseInfo(): void From 4740b6d19ef6868584f15a1fc8eae1fc0a209721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Feb 2026 22:09:36 +0100 Subject: [PATCH 3/3] Show in part info page whether price is inclusive VAT or not --- templates/helper.twig | 10 +++- templates/parts/info/_order_infos.html.twig | 59 +++++++++++---------- translations/messages.en.xlf | 12 +++++ 3 files changed, 52 insertions(+), 29 deletions(-) diff --git a/templates/helper.twig b/templates/helper.twig index 66268a96..9e68d56c 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -192,7 +192,7 @@ {% set preview_attach = part_preview_generator.tablePreviewAttachment(part) %} {% if preview_attach %} Part image + {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ attachment_thumbnail(preview_attach) }}"> {% endif %} {{ part.name }} {% endmacro %} @@ -241,3 +241,11 @@ {{ datetime|format_datetime }} {% endif %} {% endmacro %} + +{% macro vat_text(bool) %} + {% if bool === true %} + ({% trans %}prices.incl_vat{% endtrans %}) + {% elseif bool === false %} + ({% trans %}prices.excl_vat{% endtrans %}) + {% endif %} +{% endmacro %} diff --git a/templates/parts/info/_order_infos.html.twig b/templates/parts/info/_order_infos.html.twig index 68462de5..59b904df 100644 --- a/templates/parts/info/_order_infos.html.twig +++ b/templates/parts/info/_order_infos.html.twig @@ -24,8 +24,8 @@ {% if order.pricedetails is not empty %} - - +
+ @@ -36,32 +36,35 @@ {% endif %} - - - {% for detail in order.pricedetails %} - + + + {% for detail in order.pricedetails %} + {# @var detail App\Entity\PriceInformations\Pricedetail #} + - - - - - {% endfor %} - -
{% trans %}part.order.minamount{% endtrans %} {% trans %}part.order.price{% endtrans %}
- {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} - - {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} - {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %} - {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} - ({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) - {% endif %} - - {{ detail.PricePerUnit | format_money(detail.currency) }} - {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %} - {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} - ({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) - {% endif %} -
+ + {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} + + + {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} + {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %} + {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} + ({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) + {% endif %} + {{- helper.vat_text(detail.includesVAT) -}} + + + {{ detail.PricePerUnit | format_money(detail.currency) }} + {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %} + {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} + ({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) + {% endif %} + {{- helper.vat_text(detail.includesVAT) -}} + + + {% endfor %} + + {% endif %} {# Action for order information #} @@ -80,4 +83,4 @@ {% endfor %} -
\ No newline at end of file + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 2310286c..a776eb9d 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12443,5 +12443,17 @@ Buerklin-API Authentication server: Prices include VAT + + + prices.incl_vat + Incl. VAT + + + + + prices.excl_vat + Excl. VAT + +