diff --git a/assets/css/app.css b/assets/css/app.css index 135ad25c..1a5d7f5e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -620,6 +620,10 @@ BS 4 overrides } } +.form-group > label { + font-weight: bold; +} + label:not(.form-check-label, custom-control-label) { font-weight: bold; } diff --git a/composer.json b/composer.json index 7377f01e..acee1f3e 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,6 @@ "doctrine/annotations": "^1.6", "florianv/swap": "^4.0", "friendsofsymfony/ckeditor-bundle": "^2.0", - "gerardojbaez/money": "^0.3.1", "nyholm/psr7": "^1.1", "ocramius/proxy-manager": "2.1.*", "omines/datatables-bundle": "^0.2.2", @@ -43,7 +42,8 @@ "twig/extensions": "^1.5", "twig/extra-bundle": "3.x-dev", "twig/intl-extra": "3.x-dev", - "webmozart/assert": "^1.4" + "webmozart/assert": "^1.4", + "ext-bcmath": "*" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index f988abc9..52855fe3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f66e6f1196a0dbebea3453e306b7e58f", + "content-hash": "ce7f2c4f8eb45f90abbc99599f3fbe83", "packages": [ { "name": "clue/stream-filter", @@ -1570,54 +1570,6 @@ ], "time": "2019-04-15T16:29:43+00:00" }, - { - "name": "gerardojbaez/money", - "version": "v0.3.1", - "source": { - "type": "git", - "url": "https://github.com/gerardojbaez/money.git", - "reference": "1a29ca19899fad8ae559e9f2c982815ea21f8a6c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/gerardojbaez/money/zipball/1a29ca19899fad8ae559e9f2c982815ea21f8a6c", - "reference": "1a29ca19899fad8ae559e9f2c982815ea21f8a6c", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "phpunit/phpunit": "5.4.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "Gerardojbaez\\Money\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gerardo Báez", - "email": "gerardojbaez@gmail.com" - } - ], - "description": "A simple and cross-platform alternative to PHP money_format(). 91 currencies supported, including INR.", - "keywords": [ - "currency", - "formatter", - "money", - "money_format" - ], - "time": "2018-02-24T18:59:00+00:00" - }, { "name": "guzzlehttp/guzzle", "version": "6.3.3", @@ -7379,16 +7331,16 @@ }, { "name": "zendframework/zend-code", - "version": "3.3.1", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-code.git", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb" + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-code/zipball/c21db169075c6ec4b342149f446e7b7b724f95eb", - "reference": "c21db169075c6ec4b342149f446e7b7b724f95eb", + "url": "https://api.github.com/repos/zendframework/zend-code/zipball/936fa7ad4d53897ea3e3eb41b5b760828246a20b", + "reference": "936fa7ad4d53897ea3e3eb41b5b760828246a20b", "shasum": "" }, "require": { @@ -7396,10 +7348,10 @@ "zendframework/zend-eventmanager": "^2.6 || ^3.0" }, "require-dev": { - "doctrine/annotations": "~1.0", + "doctrine/annotations": "^1.0", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "zendframework/zend-coding-standard": "^1.0.0", + "phpunit/phpunit": "^7.5.15", + "zendframework/zend-coding-standard": "^1.0", "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "suggest": { @@ -7422,13 +7374,13 @@ "license": [ "BSD-3-Clause" ], - "description": "provides facilities to generate arbitrary code using an object oriented interface", - "homepage": "https://github.com/zendframework/zend-code", + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", "keywords": [ + "ZendFramework", "code", - "zf2" + "zf" ], - "time": "2018-08-13T20:36:59+00:00" + "time": "2019-08-31T14:14:34+00:00" }, { "name": "zendframework/zend-eventmanager", @@ -7538,16 +7490,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "e612609022e935f3d0337c1295176505b41188c8" + "reference": "97e59c7a16464196a8b9c77c47df68e4a39a45c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/e612609022e935f3d0337c1295176505b41188c8", - "reference": "e612609022e935f3d0337c1295176505b41188c8", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/97e59c7a16464196a8b9c77c47df68e4a39a45c4", + "reference": "97e59c7a16464196a8b9c77c47df68e4a39a45c4", "shasum": "" }, "require": { @@ -7585,7 +7537,7 @@ "parser", "php" ], - "time": "2019-08-12T20:17:41+00:00" + "time": "2019-09-01T07:51:21+00:00" }, { "name": "roave/security-advisories", @@ -8476,7 +8428,8 @@ "ext-ctype": "*", "ext-iconv": "*", "ext-intl": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-bcmath": "*" }, "platform-dev": [] } diff --git a/config/services.yaml b/config/services.yaml index 2ce270f7..e2948103 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -52,6 +52,17 @@ services: arguments: $base_current: '%default_currency%' + App\Form\Type\CurrencyEntityType: + arguments: + $base_currency: '%default_currency%' + + App\Services\PricedetailHelper: + arguments: + $base_currency: '%default_currency%' + + App\Services\MoneyFormatter: + arguments: + $base_currency: '%default_currency%' # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 33eb706c..fa83c105 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -35,6 +35,7 @@ use App\Entity\Parts\Part; use App\Form\AttachmentFormType; use App\Form\Part\PartBaseType; use App\Services\AttachmentHelper; +use App\Services\PricedetailHelper; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; @@ -51,14 +52,15 @@ class PartController extends AbstractController * @param AttachmentHelper $attachmentHelper * @return \Symfony\Component\HttpFoundation\Response */ - public function show(Part $part, AttachmentHelper $attachmentHelper) + public function show(Part $part, AttachmentHelper $attachmentHelper, PricedetailHelper $pricedetailHelper) { $this->denyAccessUnlessGranted('read', $part); return $this->render('Parts/info/show_part_info.html.twig', [ 'part' => $part, - 'attachment_helper' => $attachmentHelper + 'attachment_helper' => $attachmentHelper, + 'pricedetail_helper' => $pricedetailHelper ] ); } @@ -102,7 +104,7 @@ class PartController extends AbstractController [ 'part' => $part, 'form' => $form->createView(), - 'attachment_helper' => $attachmentHelper + 'attachment_helper' => $attachmentHelper, ]); } diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 00daf724..104337c2 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -33,7 +33,7 @@ use Symfony\Component\Validator\Constraints as Assert; * Class Attachment. * * @ORM\Entity - * @ORM\Table(name="`attachements`") + * @ORM\Table(name="`attachments`") * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="class_name", type="string") * @ORM\DiscriminatorMap({"PartDB\Part" = "PartAttachment", "Part" = "PartAttachment"}) diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index 080d0cee..ac7991f1 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -56,12 +56,13 @@ use App\Entity\Base\StructuralDBElement; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use s9e\TextFormatter\Configurator\RendererGenerators\PHP\XPathConvertor\Convertors\SingleByteStringManipulation; /** * Class AttachmentType. * * @ORM\Entity(repositoryClass="App\Repository\StructuralDBElementRepository") - * @ORM\Table(name="`attachement_types`") + * @ORM\Table(name="`attachment_types`") */ class AttachmentType extends StructuralDBElement { @@ -82,6 +83,12 @@ class AttachmentType extends StructuralDBElement */ protected $parent; + /** + * @var string + * @ORM\Column(type="string", length=65535) + */ + protected $filetype_filter; + /** * Get all attachements ("Attachement" objects) with this type. * @@ -98,6 +105,27 @@ class AttachmentType extends StructuralDBElement return $this->attachments; } + /** + * Gets an filter, which file types are allowed for attachment files. + * @return string + */ + public function getFiletypeFilter(): string + { + return $this->filetype_filter; + } + + /** + * @param string $filetype_filter + * @return AttachmentType + */ + public function setFiletypeFilter(string $filetype_filter): AttachmentType + { + $this->filetype_filter = $filetype_filter; + return $this; + } + + + /** * Returns the ID as an string, defined by the element class. * This should have a form like P000014, for a part with ID 14. diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index bf678769..e31ae63a 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -128,8 +128,8 @@ class Part extends AttachmentContainingDBElement /** * @var Orderdetail[] - * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="part") - * + * @ORM\OneToMany(targetEntity="App\Entity\PriceInformations\Orderdetail", mappedBy="part", cascade={"persist", "remove"}, orphanRemoval=true) + * @Assert\Valid() * @ColumnSecurity(prefix="orderdetails", type="object") */ protected $orderdetails; @@ -243,6 +243,12 @@ class Part extends AttachmentContainingDBElement */ protected $manufacturer_product_number = ''; + /** + * @var string + * @ORM\Column(type="string", length=255, nullable=true) + */ + protected $manufacturing_status; + /** * @var bool Determines if this part entry needs review (for example, because it is work in progress) * @ORM\Column(type="boolean") @@ -273,6 +279,7 @@ class Part extends AttachmentContainingDBElement { parent::__construct(); $this->partLots = new ArrayCollection(); + $this->orderdetails = new ArrayCollection(); } /** @@ -504,6 +511,19 @@ class Part extends AttachmentContainingDBElement return $this->orderdetails; } + public function addOrderdetail(Orderdetail $orderdetail) : Part + { + $orderdetail->setPart($this); + $this->orderdetails->add($orderdetail); + return $this; + } + + public function removeOrderdetail(Orderdetail $orderdetail) : Part + { + $this->orderdetails->removeElement($orderdetail); + return $this; + } + /** * Get all devices which uses this part. * @@ -517,68 +537,6 @@ class Part extends AttachmentContainingDBElement return $this->devices; } - /** - * Get all prices of this part. - * - * This method simply gets the prices of the orderdetails and prepare them.\n - * In the returned array/string there is a price for every supplier. - * @param int $quantity this is the quantity to choose the correct priceinformation - * @param int|null $multiplier * This is the multiplier which will be applied to every single price - * * If you pass NULL, the number from $quantity will be used - * @param bool $hide_obsolete If true, prices from obsolete orderdetails will NOT be returned - * - * @return float[] all prices as an array of floats (if "$delimeter == NULL" & "$float_array == true") - * - * @throws \Exception if there was an error - */ - public function getPrices(int $quantity = 1, $multiplier = null, bool $hide_obsolete = false) : array - { - $prices = array(); - - foreach ($this->getOrderdetails($hide_obsolete) as $details) { - $prices[] = $details->getPrice($quantity, $multiplier); - } - - return $prices; - } - - /** - * Get the average price of all orderdetails. - * - * With the $multiplier you're able to multiply the price before it will be returned. - * This is useful if you want to have the price as a string with currency, but multiplied with a factor. - * - * @param int $quantity this is the quantity to choose the correct priceinformations - * @param int|null $multiplier * This is the multiplier which will be applied to every single price - * * If you pass NULL, the number from $quantity will be used - * - * @return float price (if "$as_money_string == false") - * - * @throws \Exception if there was an error - */ - public function getAveragePrice(int $quantity = 1, $multiplier = null) : ?float - { - $prices = $this->getPrices($quantity, $multiplier, true); - //Findout out - - $average_price = null; - - $count = 0; - foreach ($this->getOrderdetails() as $orderdetail) { - $price = $orderdetail->getPrice(1, null); - if (null !== $price) { - $average_price += $price; - ++$count; - } - } - - if ($count > 0) { - $average_price /= $count; - } - - return $average_price; - } - /** * Checks if this part is marked, for that it needs further review. * @return bool diff --git a/src/Entity/PriceInformations/Currency.php b/src/Entity/PriceInformations/Currency.php index b678beeb..6953d248 100644 --- a/src/Entity/PriceInformations/Currency.php +++ b/src/Entity/PriceInformations/Currency.php @@ -34,6 +34,7 @@ namespace App\Entity\PriceInformations; use App\Entity\Base\StructuralDBElement; use Doctrine\ORM\Mapping as ORM; +use s9e\TextFormatter\Configurator\TemplateNormalizations\AbstractChooseOptimization; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -47,6 +48,8 @@ use Symfony\Component\Validator\Constraints as Assert; class Currency extends StructuralDBElement { + public const PRICE_SCALE = 5; + /** * @var string The 3 letter ISO code of the currency. * @ORM\Column(type="string") @@ -55,7 +58,7 @@ class Currency extends StructuralDBElement protected $iso_code; /** - * @var float|null The exchange rate between this currency and the base currency + * @var string|null The exchange rate between this currency and the base currency * (how many base units the current currency is worth) * @ORM\Column(type="decimal", precision=11, scale=5, nullable=true) * @Assert\Positive() @@ -86,7 +89,7 @@ class Currency extends StructuralDBElement * @param string $iso_code * @return Currency */ - public function setIsoCode(string $iso_code): Currency + public function setIsoCode(?string $iso_code): Currency { $this->iso_code = $iso_code; return $this; @@ -94,34 +97,34 @@ class Currency extends StructuralDBElement /** * Returns the inverse exchange rate (how many of the current currency the base unit is worth) - * @return float|null + * @return string|null */ - public function getInverseExchangeRate(): ?float + public function getInverseExchangeRate(): ?string { $tmp = $this->getExchangeRate(); - if ($tmp === null) { + if ($tmp === null || (float) $tmp === 0) { return null; } - return 1 / $tmp; + return bcdiv(1, $tmp, static::PRICE_SCALE); } /** * Returns The exchange rate between this currency and the base currency * (how many base units the current currency is worth) - * @return float|null + * @return string|null */ - public function getExchangeRate(): ?float + public function getExchangeRate(): ?string { return $this->exchange_rate; } /** - * @param float|null $exchange_rate + * @param string|null $exchange_rate * @return Currency */ - public function setExchangeRate(?float $exchange_rate): Currency + public function setExchangeRate(?string $exchange_rate): Currency { $this->exchange_rate = $exchange_rate; return $this; diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index c092463a..ec5d31fc 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -62,24 +62,33 @@ declare(strict_types=1); namespace App\Entity\PriceInformations; use App\Entity\Base\DBElement; +use App\Entity\Base\TimestampTrait; use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; use Exception; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Validator\Constraints as Assert; /** * Class Orderdetail. * * @ORM\Table("`orderdetails`") * @ORM\Entity() + * @ORM\HasLifecycleCallbacks() */ class Orderdetail extends DBElement { + use TimestampTrait; + /** * @var Part * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="orderdetails") * @ORM\JoinColumn(name="part_id", referencedColumnName="id") + * @Assert\NotNull() */ protected $part; @@ -91,7 +100,9 @@ class Orderdetail extends DBElement protected $supplier; /** - * @ORM\OneToMany(targetEntity="Pricedetail", mappedBy="orderdetail") + * @ORM\OneToMany(targetEntity="Pricedetail", mappedBy="orderdetail", cascade={"persist", "remove"}, orphanRemoval=true) + * @Assert\Valid() + * @ORM\OrderBy({"min_discount_quantity" = "ASC"}) */ protected $pricedetails; @@ -99,25 +110,25 @@ class Orderdetail extends DBElement * @var string * @ORM\Column(type="string") */ - protected $supplierpartnr; + protected $supplierpartnr = ""; /** * @var bool * @ORM\Column(type="boolean") */ - protected $obsolete; + protected $obsolete = false; /** * @var string * @ORM\Column(type="string") + * @Assert\Url() */ - protected $supplier_product_url; + protected $supplier_product_url = ""; - /** - * @var \DateTime The date when this element was created. - * @ORM\Column(type="datetimetz", name="datetime_added") - */ - protected $addedDate; + public function __construct() + { + $this->pricedetails = new ArrayCollection(); + } /** * Returns the ID as an string, defined by the element class. @@ -151,7 +162,7 @@ class Orderdetail extends DBElement * * @return Supplier the supplier of this orderdetails */ - public function getSupplier(): Supplier + public function getSupplier(): ?Supplier { return $this->supplier; } @@ -180,17 +191,6 @@ class Orderdetail extends DBElement return (bool) $this->obsolete; } - /** - * Returns the date/time when the element was created. - * Returns null if the element was not yet saved to DB yet. - * - * @return \DateTime|null The creation time of the part. - */ - public function getAddedDate(): ?\DateTime - { - return $this->addedDate; - } - /** * Get the link to the website of the article on the suppliers website. * @@ -205,68 +205,72 @@ class Orderdetail extends DBElement return $this->supplier_product_url; } + if ($this->supplier === null) { + return ""; + } + return $this->getSupplier()->getAutoProductUrl($this->supplierpartnr); // maybe an automatic url is available... } /** * Get all pricedetails. * - * @return Pricedetail[] all pricedetails as a one-dimensional array of Pricedetails objects, + * @return Pricedetail[]|Collection all pricedetails as a one-dimensional array of Pricedetails objects, * sorted by minimum discount quantity - * - * @throws Exception if there was an error */ - public function getPricedetails(): PersistentCollection + public function getPricedetails(): Collection { return $this->pricedetails; } /** - * Get the price for a specific quantity. - * @param int $quantity this is the quantity to choose the correct pricedetails - * @param int|null $multiplier * This is the multiplier which will be applied to every single price - * * If you pass NULL, the number from $quantity will be used - * - * @return float float: the price as a float number (if "$as_money_string == false") - * - * @throws Exception if there are no pricedetails for the choosed quantity - * (for example, there are only one pricedetails with the minimum discount quantity '10', - * but the choosed quantity is '5' --> the price for 5 parts is not defined!) - * @throws Exception if there was an error + * Adds an pricedetail to this orderdetail + * @param Pricedetail $pricedetail The pricedetail to add + * @return Orderdetail */ - public function getPrice(int $quantity = 1, $multiplier = null) : ?float + public function addPricedetail(Pricedetail $pricedetail) : Orderdetail { + $pricedetail->setOrderdetail($this); + $this->pricedetails->add($pricedetail); + return $this; + } - if (($quantity === 0) && ($multiplier === null)) { - return 0.0; + /** + * Removes an pricedetail from this orderdetail + * @param Pricedetail $pricedetail + * @return Orderdetail + */ + public function removePricedetail(Pricedetail $pricedetail) : Orderdetail + { + $this->pricedetails->removeElement($pricedetail); + return $this; + } + + /** + * Get the pricedetail for a specific quantity. + * @param float $quantity this is the quantity to choose the correct pricedetails + * + * @return Pricedetail|null: the price as a bcmath string. Null if there are no orderdetails for the given quantity + */ + public function getPrice(float $quantity = 1) : ?Pricedetail + { + if ($quantity <= 0) { + return null; } $all_pricedetails = $this->getPricedetails(); - if (count($all_pricedetails) == 0) { - return null; - } - - - $correct_pricedetails = null; - foreach ($all_pricedetails as $pricedetails) { + $correct_pricedetail = null; + foreach ($all_pricedetails as $pricedetail) { // choose the correct pricedetails for the choosed quantity ($quantity) - if ($quantity < $pricedetails->getMinDiscountQuantity()) { + if ($quantity < $pricedetail->getMinDiscountQuantity()) { break; } - $correct_pricedetails = $pricedetails; + $correct_pricedetail = $pricedetail; } - if ($correct_pricedetails === null) { - return null; - } - - if ($multiplier === null) { - $multiplier = $quantity; - } - - return $correct_pricedetails->getPricePerUnit($multiplier); + return $correct_pricedetail; } /******************************************************************************** @@ -275,6 +279,15 @@ class Orderdetail extends DBElement * *********************************************************************************/ + /** + * Sets a new part with which this orderdetail is associated + * @param Part $part + */ + public function setPart(Part $part) + { + $this->part = $part; + } + /** * Sets the new supplier associated with this orderdetail. * @param Supplier $new_supplier @@ -321,6 +334,11 @@ class Orderdetail extends DBElement */ public function setSupplierProductUrl(string $new_url) { + //Only change the internal URL if it is not the auto generated one + if ($new_url == $this->supplier->getAutoProductUrl($this->getSupplierPartNr())) { + return $this; + } + $this->supplier_product_url = $new_url; return $this; diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index 49f10734..557183d2 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -62,6 +62,7 @@ declare(strict_types=1); namespace App\Entity\PriceInformations; use App\Entity\Base\DBElement; +use App\Entity\Base\TimestampTrait; use App\Validator\Constraints\Selectable; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -72,23 +73,30 @@ use Symfony\Component\Validator\Constraints as Assert; * * @ORM\Entity() * @ORM\Table("`pricedetails`") + * @ORM\HasLifecycleCallbacks() * @UniqueEntity(fields={"orderdetail", "min_discount_quantity"}) */ class Pricedetail extends DBElement { + + public const PRICE_PRECISION = 5; + + use TimestampTrait; + /** * @var Orderdetail * @ORM\ManyToOne(targetEntity="Orderdetail", inversedBy="pricedetails") * @ORM\JoinColumn(name="orderdetails_id", referencedColumnName="id") + * @Assert\NotNull() */ protected $orderdetail; /** - * @var float The price related to the detail. (Given in the selected currency) + * @var string The price related to the detail. (Given in the selected currency) * @ORM\Column(type="decimal", precision=11, scale=5) * @Assert\Positive() */ - protected $price; + protected $price = "0.0"; /** * @var ?Currency The currency used for the current price information. @@ -100,28 +108,29 @@ class Pricedetail extends DBElement protected $currency; /** - * @var int - * @ORM\Column(type="integer") + * @var float + * @ORM\Column(type="float") * @Assert\Positive() */ - protected $price_related_quantity; + protected $price_related_quantity = 1.0; /** - * @var int - * @ORM\Column(type="integer") + * @var float + * @ORM\Column(type="float") + * @Assert\Positive() */ - protected $min_discount_quantity; + protected $min_discount_quantity = 1.0; /** * @var bool * @ORM\Column(type="boolean") */ - protected $manual_input; + protected $manual_input = true; - /** - * @ORM\Column(type="datetimetz") - */ - protected $last_modified; + public function __construct() + { + bcscale(static::PRICE_PRECISION); + } /******************************************************************************** * @@ -144,39 +153,69 @@ class Pricedetail extends DBElement * It is given in current currency and for the price related quantity. * @return float */ - public function getPrice() : float + public function getPriceFloat() : float { return (float) $this->price; } + /** + * Returns the price associated with this pricedetail. + * It is given in current currency and for the price related quantity. + * @return string The price as string, like returned raw from DB. + */ + public function getPrice() : string + { + return $this->price; + } + + /** + * Returns the price associated with this pricedetail as integer. + * It is given in current currency and for the price related quantity, in parts of 0.00001 (5 digits) + * @return int + */ + public function getPriceInt() : int + { + return (int) str_replace('.', '', $this->price); + } + /** * Get the price for a single unit in the currency associated with this price detail. * - * @param int $multiplier The returned price (float or string) will be multiplied + * @param float|string $multiplier The returned price (float or string) will be multiplied * with this multiplier. * * You will get the price for $multiplier parts. If you want the price which is stored * in the database, you have to pass the "price_related_quantity" count as $multiplier. * - * @return float the price as a float number + * @return string the price as a bcmath string */ - public function getPricePerUnit(int $multiplier = 1) : float + public function getPricePerUnit($multiplier = 1.0) : string { - return ($this->price * $multiplier) / $this->price_related_quantity; + $multiplier = (string) $multiplier; + $tmp = bcmul($this->price, $multiplier, static::PRICE_PRECISION); + return bcdiv($tmp, (string) $this->price_related_quantity, static::PRICE_PRECISION); + //return ($this->price * $multiplier) / $this->price_related_quantity; } /** * Get the price related quantity. * * This is the quantity, for which the price is valid. + * The amount is measured in part unit. * - * @return int the price related quantity + * @return float the price related quantity * * @see Pricedetail::setPriceRelatedQuantity() */ - public function getPriceRelatedQuantity(): int + public function getPriceRelatedQuantity(): float { + if ($this->orderdetail && $this->orderdetail->getPart()) { + if (!$this->orderdetail->getPart()->useFloatAmount()) { + $tmp = round($this->price_related_quantity); + return $tmp < 1 ? 1 : $tmp; + } + } return $this->price_related_quantity; } @@ -186,12 +225,21 @@ class Pricedetail extends DBElement * "Minimum discount quantity" means the minimum order quantity for which the price * of this orderdetails is valid. * + * The amount is measured in part unit. + * * @return int the minimum discount quantity * * @see Pricedetail::setMinDiscountQuantity() */ - public function getMinDiscountQuantity(): int + public function getMinDiscountQuantity(): float { + if ($this->orderdetail && $this->orderdetail->getPart()) { + if (!$this->orderdetail->getPart()->useFloatAmount()) { + $tmp = round($this->min_discount_quantity); + return $tmp < 1 ? 1 : $tmp; + } + } + return $this->min_discount_quantity; } @@ -211,6 +259,17 @@ class Pricedetail extends DBElement * *********************************************************************************/ + /** + * Sets the orderdetail to which this pricedetail belongs to. + * @param Orderdetail $orderdetail + * @return $this + */ + public function setOrderdetail(Orderdetail $orderdetail) + { + $this->orderdetail = $orderdetail; + return $this; + } + /** * Sets the currency associated with the price informations. * Set to null, to use the global base currency. @@ -226,7 +285,7 @@ class Pricedetail extends DBElement /** * Set the price. * - * @param float $new_price the new price as a float number + * @param string $new_price the new price as a float number * * * This is the price for "price_related_quantity" parts!! * * Example: if "price_related_quantity" is '10', @@ -234,7 +293,7 @@ class Pricedetail extends DBElement * * @return self */ - public function setPrice(float $new_price): Pricedetail + public function setPrice(string $new_price): Pricedetail { //Assert::natural($new_price, 'The new price must be positive! Got %s!'); @@ -256,11 +315,8 @@ class Pricedetail extends DBElement * * @return self */ - public function setPriceRelatedQuantity(int $new_price_related_quantity): self + public function setPriceRelatedQuantity(float $new_price_related_quantity): self { - //Assert::greaterThan($new_price_related_quantity, 0, - // 'The new price related quantity must be greater zero! Got %s.'); - $this->price_related_quantity = $new_price_related_quantity; return $this; @@ -285,11 +341,8 @@ class Pricedetail extends DBElement * * @return self */ - public function setMinDiscountQuantity(int $new_min_discount_quantity): self + public function setMinDiscountQuantity(float $new_min_discount_quantity): self { - //Assert::greaterThan($new_min_discount_quantity, 0, - // 'The new minimum discount quantity must be greater zero! Got %s.'); - $this->min_discount_quantity = $new_min_discount_quantity; return $this; @@ -303,6 +356,6 @@ class Pricedetail extends DBElement */ public function getIDString(): string { - return 'PD'.sprintf('%06d', $this->getID()); + return 'PD' . sprintf('%06d', $this->getID()); } } diff --git a/src/Form/AdminPages/CurrencyAdminForm.php b/src/Form/AdminPages/CurrencyAdminForm.php index 56478c11..fcfc148a 100644 --- a/src/Form/AdminPages/CurrencyAdminForm.php +++ b/src/Form/AdminPages/CurrencyAdminForm.php @@ -45,6 +45,7 @@ class CurrencyAdminForm extends BaseEntityAdminForm $is_new = $entity->getID() === null; $builder->add('iso_code', CurrencyType::class , ['required' => true, + 'required' => false, 'label' => 'currency.iso_code.label', 'preferred_choices' => ['EUR', 'USD', 'GBP', 'JPY', 'CNY'], 'attr' => ['class' => 'selectpicker', 'data-live-search' => true], diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php new file mode 100644 index 00000000..300ba0fe --- /dev/null +++ b/src/Form/Part/OrderdetailType.php @@ -0,0 +1,116 @@ +getData(); + + $builder->add('supplierpartnr', TextType::class, [ + 'label' => 'orderdetails.edit.supplierpartnr', + 'required' => false, + 'empty_data' => "" + ]); + + $builder->add('supplier', StructuralEntityType::class, [ + 'class' => Supplier::class, 'disable_not_selectable' => true, + 'label' => 'orderdetails.edit.supplier' + ]); + + $builder->add('supplier_product_url', UrlType::class, [ + 'required' => false, + 'empty_data' => "", + 'label' => 'orderdetails.edit.url' + ]); + + $builder->add('obsolete', CheckboxType::class, [ + 'required' => false, + 'label_attr' => ['class' => 'checkbox-custom'], + 'label' => 'orderdetails.edit.obsolete' + ]); + + + //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) { + /** @var Orderdetail $orderdetail */ + $orderdetail = $event->getData(); + + $dummy_pricedetail = new Pricedetail(); + if ($orderdetail->getSupplier() !== null) { + $dummy_pricedetail->setCurrency($orderdetail->getSupplier()->getDefaultCurrency()); + } + + //Attachment section + $event->getForm()->add('pricedetails', CollectionType::class, [ + 'entry_type' => PricedetailType::class, + 'allow_add' => true, 'allow_delete' => true, + 'label' => false, + 'prototype_data' => $dummy_pricedetail, + 'by_reference' => false, + 'entry_options' => [ + 'measurement_unit' => $options['measurement_unit'] + ] + ]); + + }); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Orderdetail::class, + 'error_bubbling' => false, + ]); + + $resolver->setRequired('measurement_unit'); + $resolver->setAllowedTypes('measurement_unit', [MeasurementUnit::class, 'null']); + } +} \ No newline at end of file diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index a708587d..dceb544e 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -38,6 +38,7 @@ use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; use App\Entity\Parts\Storelocation; +use App\Entity\PriceInformations\Orderdetail; use App\Form\AttachmentFormType; use App\Form\AttachmentType; use App\Form\Type\SIUnitType; @@ -144,6 +145,18 @@ class PartBaseType extends AbstractType 'by_reference' => false ]); + //Attachment section + $builder->add('orderdetails', CollectionType::class, [ + 'entry_type' => OrderdetailType::class, + 'allow_add' => true, 'allow_delete' => true, + 'label' => false, + 'by_reference' => false, + 'prototype_data' => new Orderdetail(), + 'entry_options' => [ + 'measurement_unit' => $part->getPartUnit() + ] + ]); + $builder //Buttons ->add('save', SubmitType::class, ['label' => 'part.edit.save']) diff --git a/src/Form/Part/PricedetailType.php b/src/Form/Part/PricedetailType.php new file mode 100644 index 00000000..3044c321 --- /dev/null +++ b/src/Form/Part/PricedetailType.php @@ -0,0 +1,79 @@ +add("min_discount_quantity", SIUnitType::class, [ + 'measurement_unit' => $options['measurement_unit'], + 'attr' => ['class' => 'form-control-sm'] + ]); + $builder->add("price_related_quantity", SIUnitType::class, [ + 'measurement_unit' => $options['measurement_unit'], + 'attr' => ['class' => 'form-control-sm'] + ]); + $builder->add("price", NumberType::class, [ + 'scale' => 5, + 'html5' => true, + 'attr' => ['min' => 0, 'step' => "any"] + ]); + $builder->add("currency", CurrencyEntityType::class, ['required' => false]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Pricedetail::class, + 'error_bubbling' => false + ]); + + $resolver->setRequired('measurement_unit'); + $resolver->setAllowedTypes('measurement_unit', [MeasurementUnit::class, 'null']); + } +} \ No newline at end of file diff --git a/src/Form/Type/CurrencyEntityType.php b/src/Form/Type/CurrencyEntityType.php new file mode 100644 index 00000000..a93292a6 --- /dev/null +++ b/src/Form/Type/CurrencyEntityType.php @@ -0,0 +1,118 @@ +base_currency = $base_currency; + } + + public function configureOptions(OptionsResolver $resolver) + { + //Important to call the parent resolver! + parent::configureOptions($resolver); + + $resolver->setDefault('class', Currency::class); + $resolver->setDefault('disable_not_selectable', true); + + // This options allows you to override the currency shown for the null value + $resolver->setDefault('base_currency', null); + + $resolver->setDefault('empty_message', function (Options $options) { + //By default we use the global base currency: + $iso_code = $this->base_currency; + + if ($options['base_currency']) { //Allow to override it + $iso_code = $options['base_currency']; + } + + return Currencies::getSymbol($iso_code); + }); + } + + public function generateChoiceLabels(StructuralDBElement $choice, $key, $value): string + { + //Similar to StructuralEntityType, but we use the currency symbol instead if available + + /** @var StructuralDBElement|null $parent */ + $parent = $this->options['subentities_of']; + + /*** @var Currency $choice */ + $level = $choice->getLevel(); + //If our base entity is not the root level, we need to change the level, to get zero position + if ($this->options['subentities_of'] !== null) { + $level -= $parent->getLevel() - 1; + } + + + $tmp = str_repeat(' ', $choice->getLevel()); //Use 3 spaces for intendation + if (empty($choice->getIsoCode())) { + $tmp .= htmlspecialchars($choice->getName()); + } else { + $tmp .= Currencies::getSymbol($choice->getIsoCode()); + } + return $tmp; + } + + protected function generateChoiceAttr(StructuralDBElement $choice, $key, $value) : array + { + /** @var Currency $choice */ + + $tmp = array(); + + if (!empty($choice->getIsoCode())) { + //Show the name of the currency + $tmp += ['data-subtext' => $choice->getName()]; + } + + //Disable attribute if the choice is marked as not selectable + if ($this->options['disable_not_selectable'] && $choice->isNotSelectable()) { + $tmp += ['disabled' => 'disabled']; + } + return $tmp; + } +} \ No newline at end of file diff --git a/src/Form/Type/SIUnitType.php b/src/Form/Type/SIUnitType.php index 91af5d68..6e8c1053 100644 --- a/src/Form/Type/SIUnitType.php +++ b/src/Form/Type/SIUnitType.php @@ -126,6 +126,13 @@ class SIUnitType extends AbstractType implements DataMapperInterface public function buildView(FormView $view, FormInterface $form, array $options) { + $view->vars['sm'] = false; + + //Check if we need to make this thing small + if (isset($options['attr']['class'])) { + $view->vars['sm'] = (strpos($options['attr']['class'], 'form-control-sm') !== false); + } + $view->vars['unit'] = $options['unit']; parent::buildView($view, $form, $options); // TODO: Change the autogenerated stub } diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index 44c7b5b5..a8939740 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -77,14 +77,14 @@ class StructuralEntityType extends AbstractType function ($value) use ($options){ return $this->transform($value, $options); }, function ($value) use ($options) { - return $this->reverseTransform($value, $options); + return $this->reverseTransform($value, $options); })); } public function configureOptions(OptionsResolver $resolver) { $resolver->setRequired(['class']); - $resolver->setDefaults(['attr' => ['class' => 'selectpicker', 'data-live-search' => true], + $resolver->setDefaults([ 'show_fullpath_in_subtext' => true, //When this is enabled, the full path will be shown in subtext 'subentities_of' => null, //Only show entities with the given parent class 'disable_not_selectable' => false, //Disable entries with not selectable property @@ -99,6 +99,16 @@ class StructuralEntityType extends AbstractType return $this->generateChoiceAttr($choice, $key, $value); } ]); + + $resolver->setDefault('empty_message', null); + + $resolver->setDefault('attr', function (Options $options) { + $tmp = ['class' => 'selectpicker', 'data-live-search' => true]; + if ($options['empty_message']) { + $tmp['data-none-Selected-Text'] = $options['empty_message']; + } + return $tmp; + }); } protected function generateChoiceAttr(StructuralDBElement $choice, $key, $value) : array @@ -130,7 +140,7 @@ class StructuralEntityType extends AbstractType $tmp = str_repeat(' ', $choice->getLevel()); //Use 3 spaces for intendation - $tmp .= htmlspecialchars($choice->getName($parent)); + $tmp .= htmlspecialchars($choice->getName()); return $tmp; } diff --git a/src/Migrations/Version20190829104643.php b/src/Migrations/Version20190829104643.php new file mode 100644 index 00000000..5947939d --- /dev/null +++ b/src/Migrations/Version20190829104643.php @@ -0,0 +1,98 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + //Add timestamps to orderdetails and pricedetails + + $this->addSql('ALTER TABLE attachements CHANGE element_id element_id INT DEFAULT NULL, CHANGE type_id type_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE attachement_types CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE devices CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE device_parts CHANGE id_part id_part INT DEFAULT NULL, CHANGE id_device id_device INT DEFAULT NULL'); + $this->addSql('ALTER TABLE categories CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE footprints CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE filename filename MEDIUMTEXT NOT NULL, CHANGE filename_3d filename_3d MEDIUMTEXT NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE manufacturers CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE measurement_units CHANGE unit unit VARCHAR(255) DEFAULT NULL, CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE measurement_units ADD CONSTRAINT FK_F5AF83CF727ACA70 FOREIGN KEY (parent_id) REFERENCES `measurement_units` (id)'); + $this->addSql('CREATE INDEX IDX_F5AF83CF727ACA70 ON measurement_units (parent_id)'); + $this->addSql('ALTER TABLE parts ADD manufacturing_status VARCHAR(255) DEFAULT NULL, CHANGE id_category id_category INT DEFAULT NULL, CHANGE id_footprint id_footprint INT DEFAULT NULL, CHANGE order_orderdetails_id order_orderdetails_id INT DEFAULT NULL, CHANGE id_manufacturer id_manufacturer INT DEFAULT NULL, CHANGE id_master_picture_attachement id_master_picture_attachement INT DEFAULT NULL, CHANGE id_part_unit id_part_unit INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE mass mass DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots CHANGE id_store_location id_store_location INT DEFAULT NULL, CHANGE id_part id_part INT DEFAULT NULL, CHANGE expiration_date expiration_date DATETIME DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE storelocations CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE storage_type_id storage_type_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE suppliers CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE default_currency_id default_currency_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE shipping_costs shipping_costs NUMERIC(11, 5) DEFAULT NULL'); + $this->addSql('ALTER TABLE currencies CHANGE exchange_rate exchange_rate NUMERIC(11, 5) DEFAULT NULL, CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE currencies ADD CONSTRAINT FK_37C44693727ACA70 FOREIGN KEY (parent_id) REFERENCES currencies (id)'); + $this->addSql('CREATE INDEX IDX_37C44693727ACA70 ON currencies (parent_id)'); + $this->addSql('ALTER TABLE orderdetails ADD last_modified DATETIME NOT NULL, CHANGE part_id part_id INT DEFAULT NULL, CHANGE id_supplier id_supplier INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE pricedetails ADD datetime_added DATETIME NOT NULL, CHANGE orderdetails_id orderdetails_id INT DEFAULT NULL, CHANGE id_currency id_currency INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `groups` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE users CHANGE group_id group_id INT DEFAULT NULL, CHANGE password password VARCHAR(255) DEFAULT NULL, CHANGE first_name first_name VARCHAR(255) DEFAULT NULL, CHANGE last_name last_name VARCHAR(255) DEFAULT NULL, CHANGE department department VARCHAR(255) DEFAULT NULL, CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE config_language config_language VARCHAR(255) DEFAULT NULL, CHANGE config_timezone config_timezone VARCHAR(255) DEFAULT NULL, CHANGE config_theme config_theme VARCHAR(255) DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + + //Use float values for pricedetails amount, to use with non integer part units + $this->addSql('ALTER TABLE pricedetails CHANGE orderdetails_id orderdetails_id INT DEFAULT NULL, CHANGE id_currency id_currency INT DEFAULT NULL, CHANGE price_related_quantity price_related_quantity DOUBLE PRECISION NOT NULL, CHANGE min_discount_quantity min_discount_quantity DOUBLE PRECISION NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + + //Fix typo in attachment table names + $this->addSql("ALTER TABLE attachements RENAME TO attachments;"); + $this->addSql("ALTER TABLE attachement_types RENAME TO attachment_types;"); + + //Add an filetye filter field to attachment types + $this->addSql('ALTER TABLE attachment_types ADD filetype_filter VARCHAR(65535) NOT NULL, CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + + + //Fill empty timestamps with current date + $tables = ["attachments", "attachment_types", "categories", "devices", "footprints", "manufacturers", + "orderdetails", "pricedetails", "storelocations", "suppliers"]; + + foreach ($tables as $table) { + $this->addSql("UPDATE $table SET datetime_added = NOW() WHERE datetime_added = '0000-00-00 00:00:00'"); + $this->addSql("UPDATE $table SET last_modified = datetime_added WHERE last_modified = '0000-00-00 00:00:00'"); + } + + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('ALTER TABLE `attachement_types` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `attachements` CHANGE type_id type_id INT DEFAULT NULL, CHANGE element_id element_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `categories` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE currencies DROP FOREIGN KEY FK_37C44693727ACA70'); + $this->addSql('DROP INDEX IDX_37C44693727ACA70 ON currencies'); + $this->addSql('ALTER TABLE currencies CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE exchange_rate exchange_rate NUMERIC(11, 5) DEFAULT \'NULL\', CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `device_parts` CHANGE id_device id_device INT DEFAULT NULL, CHANGE id_part id_part INT DEFAULT NULL'); + $this->addSql('ALTER TABLE `devices` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `footprints` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE filename filename MEDIUMTEXT NOT NULL COLLATE utf8_unicode_ci, CHANGE filename_3d filename_3d MEDIUMTEXT NOT NULL COLLATE utf8_unicode_ci, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `groups` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `manufacturers` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `measurement_units` DROP FOREIGN KEY FK_F5AF83CF727ACA70'); + $this->addSql('DROP INDEX IDX_F5AF83CF727ACA70 ON `measurement_units`'); + $this->addSql('ALTER TABLE `measurement_units` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE unit unit VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `orderdetails` DROP last_modified, CHANGE part_id part_id INT DEFAULT NULL, CHANGE id_supplier id_supplier INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE part_lots CHANGE id_store_location id_store_location INT DEFAULT NULL, CHANGE id_part id_part INT DEFAULT NULL, CHANGE expiration_date expiration_date DATETIME DEFAULT \'NULL\', CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `parts` DROP manufacturing_status, CHANGE id_category id_category INT DEFAULT NULL, CHANGE id_footprint id_footprint INT DEFAULT NULL, CHANGE id_manufacturer id_manufacturer INT DEFAULT NULL, CHANGE id_master_picture_attachement id_master_picture_attachement INT DEFAULT NULL, CHANGE order_orderdetails_id order_orderdetails_id INT DEFAULT NULL, CHANGE id_part_unit id_part_unit INT DEFAULT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE mass mass DOUBLE PRECISION DEFAULT \'NULL\''); + $this->addSql('ALTER TABLE `pricedetails` DROP datetime_added, CHANGE orderdetails_id orderdetails_id INT DEFAULT NULL, CHANGE id_currency id_currency INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `storelocations` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE storage_type_id storage_type_id INT DEFAULT NULL, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `suppliers` CHANGE parent_id parent_id INT DEFAULT NULL, CHANGE default_currency_id default_currency_id INT DEFAULT NULL, CHANGE shipping_costs shipping_costs NUMERIC(11, 5) DEFAULT \'NULL\', CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + $this->addSql('ALTER TABLE `users` CHANGE group_id group_id INT DEFAULT NULL, CHANGE password password VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE first_name first_name VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE last_name last_name VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE department department VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE email email VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE config_language config_language VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE config_timezone config_timezone VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE config_theme config_theme VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8_general_ci, CHANGE last_modified last_modified DATETIME NOT NULL, CHANGE datetime_added datetime_added DATETIME NOT NULL'); + } +} diff --git a/src/Services/MoneyFormatter.php b/src/Services/MoneyFormatter.php index 7cb984cc..a1e933c7 100644 --- a/src/Services/MoneyFormatter.php +++ b/src/Services/MoneyFormatter.php @@ -32,28 +32,44 @@ namespace App\Services; -use Gerardojbaez\Money\Money; -use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; +use App\Entity\PriceInformations\Currency; +use Locale; class MoneyFormatter { - private $params; + protected $base_currency; + protected $locale; - public function __construct(ContainerBagInterface $params) + public function __construct(string $base_currency) { - $this->params = $params; + $this->base_currency = $base_currency; + $this->locale = Locale::getDefault(); } - public function format($amount, string $currency = "") : string + /** + * Format the the given value in the given currency + * @param string|float $value The value that should be formatted. + * @param Currency|null $currency The currency that should be used for formatting. If null the global one is used + * @param int $decimals The number of decimals that should be shown. + * @param bool $show_all_digits If set to true, all digits are shown, even if they are null. + * @return string + */ + public function format($value, ?Currency $currency = null, $decimals = 5, bool $show_all_digits = false) { - if ($currency === "") { - $currency = $this->params->get("default_currency"); + $iso_code = $this->base_currency; + if ($currency !== null && !empty($currency->getIsoCode())) { + $iso_code = $currency->getIsoCode(); } - $money = new Money($amount, $currency); + $number_formatter = new \NumberFormatter($this->locale, \NumberFormatter::CURRENCY); + if ($show_all_digits) { + $number_formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $decimals); + } else { + $number_formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $decimals); + } - return $money->format(); + return $number_formatter->formatCurrency((float) $value, $iso_code); } } \ No newline at end of file diff --git a/src/Services/PricedetailHelper.php b/src/Services/PricedetailHelper.php new file mode 100644 index 00000000..5dbbfee8 --- /dev/null +++ b/src/Services/PricedetailHelper.php @@ -0,0 +1,206 @@ +base_currency = $base_currency; + $this->locale = Locale::getDefault(); + } + + /** + * Determines the highest amount, for which you get additional discount. + * This function determines the highest min_discount_quantity for the given part. + * @param Part $part + * @return float|null + */ + public function getMaxDiscountAmount(Part $part) : ?float + { + $orderdetails = $part->getOrderdetails(true); + + $max = 0; + + foreach ($orderdetails as $orderdetail) { + $pricedetails = $orderdetail->getPricedetails(); + //The orderdetail must have pricedetails, otherwise this will not work! + if (empty($pricedetails)) { + continue; + } + + /* Pricedetails in orderdetails are ordered by min discount quantity, + so our first object is our min order amount for the current orderdetail */ + $max_amount = $pricedetails->last()->getMinDiscountQuantity(); + + if ($max_amount > $max) { + $max = $max_amount; + } + } + + if ($max > 0) { + return $max; + } + + return null; + } + + /** + * Determines the minimum amount of the part that can be ordered + * @param Part $part The part for which the minimum order amount should be determined. + * @return float + */ + public function getMinOrderAmount(Part $part) : ?float + { + $orderdetails = $part->getOrderdetails(true); + + $min = INF; + + foreach ($orderdetails as $orderdetail) { + $pricedetails = $orderdetail->getPricedetails(); + //The orderdetail must have pricedetails, otherwise this will not work! + if (count($pricedetails) === 0) { + continue; + } + + /* Pricedetails in orderdetails are ordered by min discount quantity, + so our first object is our min order amount for the current orderdetail */ + $min_amount = $pricedetails[0]->getMinDiscountQuantity(); + + if ($min_amount < $min) { + $min = $min_amount; + } + } + + if ($min < INF) { + return $min; + } + + return null; + } + + /** + * Calculates the average price of a part, when ordering the amount $amount. + * @param Part $part The part for which the average price should be calculated. + * @param float $amount The order amount for which the average price should be calculated. + * If set to null, the mininmum order amount for the part is used. + * @param Currency|null $currency The currency in which the average price should be calculated + * @return string|null The Average price as bcmath string. Returns null, if it was not possible to calculate the + * price for the given + */ + public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null) : ?string + { + if ($amount === null) { + $amount = $this->getMinOrderAmount($part); + } + + if ($amount === null) { + return null; + } + + $orderdetails = $part->getOrderdetails(true); + + $avg = "0"; + $count = 0; + + //Find the price for the amount, for the given + foreach ($orderdetails as $orderdetail) { + $pricedetail = $orderdetail->getPrice($amount); + + //When we dont have informations about this amount, ignore it + if ($pricedetail === null) { + continue; + } + + $avg = bcadd($avg, $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency), Pricedetail::PRICE_PRECISION); + $count++; + } + + if ($count === 0) { + return null; + } + + return bcdiv($avg, (string) $count, Pricedetail::PRICE_PRECISION); + } + + /** + * Converts the given value in origin currency to the choosen target currency + * @param $value float|string The value that should be converted + * @param Currency|null $originCurrency The currency the $value is given in. + * Set to null, to use global base currency. + * @param Currency|null $targetCurrency The target currency, to which $value should be converted. + * Set to null, to use global base currency. + * @return string|null The value in $targetCurrency given as bcmath string. + * Returns null, if it was not possible to convert between both values (e.g. when the exchange rates are missing) + */ + public function convertMoneyToCurrency($value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null) : ?string + { + //Skip conversion, if both currencies are same + if ($originCurrency === $targetCurrency) { + return $value; + } + + $value = (string) $value; + + //Convert value to base currency + $val_base = $value; + if ($originCurrency !== null) { + //Without an exchange rate we can not calculate the exchange rate + if ((float) $originCurrency->getExchangeRate() === 0) { + return null; + } + + $val_base = bcmul($value, $originCurrency->getExchangeRate(), Pricedetail::PRICE_PRECISION); + } + + //Convert value in base currency to target currency + $val_target = $val_base; + if ($targetCurrency !== null) { + //Without an exchange rate we can not calculate the exchange rate + if ($targetCurrency->getExchangeRate() === null) { + return null; + } + + $val_target = bcmul($val_base, $targetCurrency->getInverseExchangeRate(), Pricedetail::PRICE_PRECISION); + } + + return $val_target; + } +} \ No newline at end of file diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 56ccd17d..ff92f8ef 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -32,11 +32,13 @@ namespace App\Twig; use App\Entity\Attachments\Attachment; use App\Entity\Base\DBElement; use App\Entity\Parts\MeasurementUnit; +use App\Entity\PriceInformations\Currency; use App\Services\AmountFormatter; use App\Services\EntityURLGenerator; use App\Services\MoneyFormatter; use App\Services\SIFormatter; use App\Services\TreeBuilder; +use Money\Currencies; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Serializer\SerializerInterface; use Twig\Extension\AbstractExtension; @@ -123,14 +125,14 @@ class AppExtension extends AbstractExtension return $item->get(); } - public function formatCurrency($amount, $currency = "") + public function formatCurrency($amount, Currency $currency = null, int $decimals = 5) { - return $this->moneyFormatter->format($amount, $currency); + return $this->moneyFormatter->format($amount, $currency, $decimals); } - public function siFormat($value, $unit, $decimals = 2) + public function siFormat($value, $unit, $decimals = 2, bool $show_all_digits = false) { - return $this->siformatter->format($value, $unit, $decimals); + return $this->siformatter->format($value, $unit, $decimals, $show_all_digits); } public function amountFormat($value, ?MeasurementUnit $unit, array $options = []) diff --git a/symfony.lock b/symfony.lock index eb11f259..561a210c 100644 --- a/symfony.lock +++ b/symfony.lock @@ -117,9 +117,6 @@ "./config/packages/fos_ckeditor.yaml" ] }, - "gerardojbaez/money": { - "version": "v0.3.1" - }, "guzzlehttp/guzzle": { "version": "6.3.3" }, diff --git a/templates/Form/extendedBootstrap4_layout.html.twig b/templates/Form/extendedBootstrap4_layout.html.twig index 4f70adf0..a2279744 100644 --- a/templates/Form/extendedBootstrap4_layout.html.twig +++ b/templates/Form/extendedBootstrap4_layout.html.twig @@ -32,11 +32,11 @@ {%- endblock choice_widget_options -%} {% block si_unit_widget %} -