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 + +