Merge branch 'orderdetails_edit'

This commit is contained in:
Jan Böhmer 2019-09-01 22:05:30 +02:00
commit e0b171d240
32 changed files with 1142 additions and 268 deletions

View file

@ -620,6 +620,10 @@ BS 4 overrides
}
}
.form-group > label {
font-weight: bold;
}
label:not(.form-check-label, custom-control-label) {
font-weight: bold;
}

View file

@ -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",

85
composer.lock generated
View file

@ -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": []
}

View file

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

View file

@ -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,
]);
}

View file

@ -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"})

View file

@ -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.

View file

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

View file

@ -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;

View file

@ -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;

View file

@ -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());
}
}

View file

@ -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],

View file

@ -0,0 +1,116 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 Jan Böhmer
* https://github.com/jbtronics
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Form\Part;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\UrlType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function foo\func;
class OrderdetailType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var Orderdetail $orderdetail */
$orderdetail = $builder->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']);
}
}

View file

@ -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'])

View file

@ -0,0 +1,79 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 Jan Böhmer
* https://github.com/jbtronics
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Form\Part;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
use App\Form\Type\CurrencyEntityType;
use App\Form\Type\SIUnitType;
use App\Form\Type\StructuralEntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PricedetailType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
//No labels needed, we define translation in templates
$builder->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']);
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 Jan Böhmer
* https://github.com/jbtronics
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Form\Type;
use App\Entity\Base\StructuralDBElement;
use App\Entity\PriceInformations\Currency;
use App\Services\TreeBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Intl\Currencies;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CurrencyEntityType extends StructuralEntityType
{
protected $base_currency;
public function __construct(EntityManagerInterface $em, TreeBuilder $builder, $base_currency)
{
parent::__construct($em, $builder);
$this->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('&nbsp;&nbsp;&nbsp;', $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;
}
}

View file

@ -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
}

View file

@ -84,7 +84,7 @@ class StructuralEntityType extends AbstractType
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('&nbsp;&nbsp;&nbsp;', $choice->getLevel()); //Use 3 spaces for intendation
$tmp .= htmlspecialchars($choice->getName($parent));
$tmp .= htmlspecialchars($choice->getName());
return $tmp;
}

View file

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20190829104643 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() 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\'.');
//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');
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,206 @@
<?php
/**
*
* part-db version 0.1
* Copyright (C) 2005 Christoph Lechner
* http://www.cl-projects.de/
*
* part-db version 0.2+
* Copyright (C) 2009 K. Jacobs and others (see authors.php)
* http://code.google.com/p/part-db/
*
* Part-DB Version 0.4+
* Copyright (C) 2016 - 2019 Jan Böhmer
* https://github.com/jbtronics
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
*/
namespace App\Services;
use App\Entity\Parts\Part;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Pricedetail;
use Locale;
class PricedetailHelper
{
protected $base_currency;
protected $locale;
public function __construct(string $base_currency)
{
$this->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;
}
}

View file

@ -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 = [])

View file

@ -117,9 +117,6 @@
"./config/packages/fos_ckeditor.yaml"
]
},
"gerardojbaez/money": {
"version": "v0.3.1"
},
"guzzlehttp/guzzle": {
"version": "6.3.3"
},

View file

@ -32,11 +32,11 @@
{%- endblock choice_widget_options -%}
{% block si_unit_widget %}
<div class="input-group">
<div class="input-group {% if sm %}input-group-sm{% endif %}">
{{ form_widget(form.value) }}
<div class="input-group-append">
<div class="input-group-append {% if sm %}input-group-sm{% endif %}">
{% if form.prefix is defined %}
{{ form_widget(form.prefix, {'attr': {'class': 'custom-select btn'}}) }}
{{ form_widget(form.prefix, {'attr': {'class': 'custom-select'}}) }}
{% endif %}
{% if unit is not empty %}
<label class="input-group-text">{{ unit }}</label>

View file

@ -6,7 +6,7 @@
{% endset %}
<table class="table table-striped" id="attachments_table" data-prototype="{{ form_widget(form.attachments.vars.prototype)|e('html_attr') }}">
<table class="table table-striped table-sm" id="attachments_table" data-prototype="{{ form_widget(form.attachments.vars.prototype)|e('html_attr') }}">
<tbody>
{% for attachment in form.attachments %}
<tr>

View file

@ -6,7 +6,7 @@
{% endset %}
<table class="table table-striped" id="lots_table" data-prototype="{{ form_widget(form.partLots.vars.prototype)|e('html_attr') }}">
<table class="table table-striped table-sm" id="lots_table" data-prototype="{{ form_widget(form.partLots.vars.prototype)|e('html_attr') }}">
<tbody>
{% for lot in form.partLots %}
<tr>

View file

@ -0,0 +1,84 @@
{% form_theme form with ['Parts/edit/edit_form_styles.html.twig', "bootstrap_4_layout.html.twig"] %}
<table class="table table-striped table-sm" id="orderdetails_table" data-prototype="{{ form_widget(form.orderdetails.vars.prototype)|e('html_attr') }}">
<tbody>
{% for detail in form.orderdetails %}
{{ form_widget(detail) }}
{% endfor %}
</tbody>
</table>
<button type="button" class="btn btn-success" onclick="create_orderdetail_entry(this)">
<i class="fas fa-plus-square fa-fw"></i>
{% trans %}orderdetail.create{% endtrans %}
</button>
<script>
function delete_pricedetail_entry(btn) {
window.bootbox.confirm('{% trans %}pricedetails.edit.delete.confirm{% endtrans %}', function (result) {
if(result) {
$(btn).closest("tr").remove();
}
});
}
function create_pricedetail_entry(btn) {
//Determine the table, so we can determine, how many entries there are already.
$holder = $(btn).siblings("table");
var index = $holder.find(":input").length;
var newForm = $holder.data("prototype");
//Increase the index
newForm = newForm.replace(/__name__/g, index);
//Determine the id of the fields we later update
var price_related_id = $("input[id$='price_related_quantity_value']", newForm).attr('id');
var min_discount_id = $("input[id$='min_discount_quantity_value']", newForm).attr('id');
console.error(price_related_id);
console.error(min_discount_id);
//Determine the new value for the min_discount_qty
var new_min_amount = $("input[id$='min_discount_quantity_value']" , $holder).last().val();
new_min_amount = parseInt(new_min_amount) + 1;
//Determine the next exponent, 10 -> 100 -> 1000
new_min_amount = Math.pow(10, Math.ceil(Math.log10(new_min_amount)));
$holder.children("tbody").append(newForm);
// Now really update the values, after they were inserted into dom
$("#" + min_discount_id).val(new_min_amount);
$("#" + price_related_id).val("1");
//Reinit the selectpickers
$(".selectpicker").selectpicker();
}
function delete_orderdetail_entry(btn) {
window.bootbox.confirm('{% trans %}orderdetails.edit.delete.confirm{% endtrans %}', function (result) {
if(result) {
$(btn).parents("tr").remove();
}
});
}
function create_orderdetail_entry(btn) {
//Determine the table, so we can determine, how many entries there are already.
$holder = $("#orderdetails_table");
var index = $holder.find(":input").length;
var newForm = $holder.data("prototype");
//Increase the index
newForm = newForm.replace(/__name__/g, index);
$holder.children("tbody").append(newForm);
//Reinit the selectpickers
$(".selectpicker").selectpicker();
}
</script>

View file

@ -0,0 +1,62 @@
{% block pricedetail_widget %}
{% form_theme form.currency 'Form/extendedBootstrap4_layout.html.twig' %}
<tr>
<td>{{ form_widget(form.min_discount_quantity, {'attr': {'class': 'form-control-sm'}}) }} {{ form_errors(form.min_discount_quantity) }}</td>
<td>
<div class="input-group input-group-sm">
{{ form_widget(form.price) }}
<div class="input-group-append">
{{ form_widget(form.currency, {'attr': {'class': 'selectpicker form-control-sm'}}) }}
</div>
</div>
{{ form_errors(form.price) }}
{{ form_errors(form.currency) }}
</td>
<td>{{ form_widget(form.price_related_quantity, {'attr': {'class': 'form-control-sm'}}) }} {{ form_errors(form.price_related_quantity) }}</td>
<td><button type="button" class="btn btn-danger order_btn_delete btn-sm" title="{% trans %}orderdetail.delete{% endtrans %}" onclick="delete_pricedetail_entry(this);">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
{{ form_errors(form) }}
</td>
</tr>
{% endblock %}
{% block orderdetail_widget %}
<tr>
<td>
{{ form_row(form.supplier, {'attr': {'class': 'form-control-sm'}}) }}
{{ form_row(form.supplierpartnr, {'attr': {'class': 'form-control-sm'}}) }}
{{ form_row(form.supplier_product_url, {'attr': {'class': 'form-control-sm'}}) }}
{{ form_widget(form.obsolete) }}
</td>
<td>
<table class="table table-sm table-bordered" data-prototype="{{ form_widget(form.pricedetails.vars.prototype)|e('html_attr') }}">
<thead>
<tr>
<th>{% trans %}pricedetails.edit.min_qty{% endtrans %}</th>
<th>{% trans %}pricedetails.edit.price{% endtrans %}</th>
<th>{% trans %}pricedetails.edit.price_qty{% endtrans %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for price in form.pricedetails %}
{{ form_widget(price) }}
{% endfor %}
</tbody>
</table>
<button type="button" class="btn btn-success" onclick="create_pricedetail_entry(this)">
<i class="fas fa-plus-square fa-fw"></i>
{% trans %}pricedetail.create{% endtrans %}
</button>
</td>
<td>
<button type="button" class="btn btn-danger order_btn_delete" onclick="delete_orderdetail_entry(this);" title="{% trans %}orderdetail.delete{% endtrans %}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
{{ form_errors(form) }}
</td>
</tr>
{% endblock %}

View file

@ -46,6 +46,12 @@
{% trans %}part.edit.tab.attachments{% endtrans %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" role="tab" href="#orderdetails">
<i class="fas fa-shopping-cart fa-fw"></i>
{% trans %}part.edit.tab.orderdetails{% endtrans %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" role="tab" href="#comment">
<i class="fas fa-sticky-note fa-fw"></i>
@ -72,6 +78,9 @@
<div class="tab-pane fade p-2" id="attachments" role="tabpanel">
{% include "Parts/edit/_attachments.html.twig" %}
</div>
<div class="tab-pane fade p-2" id="orderdetails" role="tabpanel">
{% include "Parts/edit/_orderdetails.html.twig" %}
</div>
<div class="tab-pane fade p-2" id="comment" role="tabpanel">
{{ form_widget(form.comment)}}
</div>
@ -81,5 +90,6 @@
</div>
{{ form_row(form.save) }}
{{ form_row(form.reset) }}
{{ form_errors(form) }}
{{ form_end(form) }}
{% endblock %}

View file

@ -34,9 +34,23 @@
<i class="fas fa-microchip fa-fw" ></i>
<span class="text-muted">{{ part.footprint.fullPath ?? "-"}}</span>
</h6>
<h6 title="{% trans %}part.avg_price.label{% endtrans %}">
<h6>
<i class="fas fa-money-bill-alt fa-fw"></i>
<span class="text-muted">{% if part.averagePrice is not null %}{{ part.averagePrice | moneyFormat }}{% else %}-{% endif %}</span>
<span class="text-muted">
{% set min_order_amount = pricedetail_helper.minOrderAmount(part) %}
{% set max_order_amount = pricedetail_helper.maxDiscountAmount(part) %}
{% set max_order_price = pricedetail_helper.calculateAvgPrice(part, max_order_amount) %}
{% if max_order_price is not null %}
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ max_order_amount | amountFormat(part.partUnit) }}">{{ max_order_price | moneyFormat }}</span>
{% if min_order_amount < max_order_amount %}
<span> - </span>
<span title="{% trans %}part.avg_price.label{% endtrans %} {{ min_order_amount | amountFormat(part.partUnit) }}">{{pricedetail_helper.calculateAvgPrice(part, min_order_amount) | moneyFormat }}</span>
{% endif %}
{% endif %}
</span>
</h6>
{#
{% if part.comment != "" %}

View file

@ -25,20 +25,31 @@
<tr>
<th>{% trans %}part.order.minamount{% endtrans %}</th>
<th>{% trans %}part.order.price{% endtrans %}</th>
<th>{% trans %}part.order.single_price{% endtrans %}</th>
<th>
{% trans %}part.order.single_price{% endtrans %}
{% if part.partUnit %}
/ 1 {{ part.partUnit.unit }}
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for detail in order.pricedetails %}
<tr>
<td>
{{ detail.MinDiscountQuantity }}
{{ detail.MinDiscountQuantity | amountFormat(part.partUnit) }}
</td>
<td>
{{ detail.Price | moneyFormat }} <i>{% trans %}part.order.price_per{% endtrans %}</i> {{ detail.PriceRelatedQuantity }}
{{ detail.price | moneyFormat(detail.currency) }} / {{ detail.PriceRelatedQuantity | amountFormat(part.partUnit) }}
{% if detail.currency and pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) > 0 %}
<span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) | moneyFormat() }})</span>
{% endif %}
</td>
<td>
{{ detail.PricePerUnit | moneyFormat}}
{{ detail.PricePerUnit | moneyFormat(detail.currency) }}
{% if detail.currency and pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) > 0 %}
<span class="text-muted">({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) | moneyFormat() }})</span>
{% endif %}
</td>
</tr>
{% endfor %}

View file

@ -1045,7 +1045,7 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</trans-unit>
<trans-unit id="cpRdMwo" resname="part.order.price_per">
<source>part.order.price_per</source>
<target state="translated">Preis pro</target>
<target state="translated">pro</target>
</trans-unit>
<trans-unit id="EKnfKFl" resname="edit.caption_short">
<source>edit.caption_short</source>