<?php
namespace App\Entity;
use App\Exception\CurrencyException;
use App\Model\Currency;
use App\Model\SalesItemDelivery;
use App\Validator\ValidSalesItemCode;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity(
* repositoryClass="App\Repository\SalesItemRepository"
* )
* @ORM\Table(
* name="sales_item",
* options={"collate"="utf8_swedish_ci"}
* )
* @ORM\HasLifecycleCallbacks
*
* @ValidSalesItemCode(
* groups={"Offer"}
* )
*/
class SalesItem implements EntityInterface
{
use ProductTrait;
const POSITION_MULTIPLIER = 1000;
/**
* @ORM\Id
* @ORM\Column(
* type="integer"
* )
* @ORM\GeneratedValue(
* strategy="AUTO"
* )
*
* @var int|null
*/
protected ?int $id = null;
/**
* @ORM\Column(
* type="string",
* length=5000,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $additionalInfo = null;
/**
* @ORM\Column(
* type="string",
* length=50,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $commodityCode = null;
/**
* Cost of sales is always in default currency EUR
*
* @var int|null
*/
protected ?int $costOfSales = null;
/**
* @ORM\Column(
* type="datetime"
* )
* @Assert\NotNull
*
* @var DateTime
*/
protected DateTime $created;
/**
* @var string
*/
protected string $currency = Currency::DEFAULT_CURRENCY;
/**
* @var float|null
*/
protected ?float $currencyExchangeRate = null;
/**
* @ORM\Column(
* name="deliveries",
* type="json",
* nullable=true
* )
*
* @var array|null
*/
protected ?array $deliveriesArray = null;
/**
* @Assert\Valid
*
* @var array<SalesItemDelivery>
*/
protected array $deliveries = [];
/**
* @ORM\Column(
* type="string",
* length=1000,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $deliveryComments = null;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $deliveryDateEstimate = null;
/**
* @ORM\Column(
* type="decimal",
* precision=5,
* scale=2,
* nullable=true
* )
* @Assert\Range(
* min=0,
* max=100
* )
*
* @var string|null
*/
protected ?string $discountPercentage = null;
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
* @Assert\Range(
* min=0,
* )
*
* @var int|null
*/
protected ?int $discountValue = null;
/**
* @ORM\Column(
* type="boolean"
* )
*
* @var bool
*/
protected bool $excludePrice = false;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $exportLicenseExpiryDate = null;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $exportLicenseIssueDate = null;
/**
* @ORM\Column(
* type="string",
* length=20,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $exportLicenseNumberEu = null;
/**
* @ORM\Column(
* type="string",
* length=20,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $exportLicenseNumberUs = null;
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $exportLicenseQuantityEu = null;
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $exportLicenseQuantityUs = null;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $featuresExpiryDate = null;
/**
* @ORM\Column(
* type="boolean"
* )
*
* @var bool
*/
protected bool $inventory = false;
/**
* @ORM\Column(
* type="string",
* length=2000,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $licenseSerialNumbers = null;
/**
* @ORM\Column(
* type="boolean"
* )
*
* @var bool
*/
protected bool $licenseVisible = false;
/**
* @ORM\Column(
* type="integer"
* )
*
* @Assert\NotNull
*
* @var int
*/
protected int $position;
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $purchasePrice = null;
/**
* @ORM\Column(
* type="integer"
* )
*
* @Assert\NotNull
*
* @var int
*/
protected int $quantity = 1;
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $quantityPerPci = null;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $serviceEnd = null;
/**
* @ORM\Column(
* type="string",
* length=50,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $serviceId = null;
/**
* @ORM\Column(
* type="boolean"
* )
*
* @var bool
*/
protected bool $servicePeriodVisible = false;
/**
* @ORM\Column(
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $serviceStart = null;
/**
* @ORM\ManyToOne(
* targetEntity="Supplier",
* inversedBy="salesItems"
* )
* @ORM\JoinColumn(
* name="supplier_id",
* referencedColumnName="id",
* onDelete="SET NULL"
* )
*
* @var Supplier|null
*/
protected ?Supplier $supplier = null;
/**
* @var int
*/
protected int $totalQuantity = 0;
/**
* @ORM\Column(
* type="string",
* length=10,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $unit = 'pc';
/**
* @ORM\Column(
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $unitPrice = null;
/**
* @ORM\Column(
* type="string",
* length=100,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $versionCode = null;
/**
* @param int $position
*/
public function __construct(int $position)
{
$this->position = $position;
$this->created = new DateTime();
}
public function __clone()
{
$this->id = null;
$this->created = new DateTime();
$this->purchasePrice = null;
}
/**
* @return string
*/
public function __toString(): string
{
return $this->code;
}
/**
* @param int $price
*
* @return int
*
* @throws CurrencyException
*/
private function convertPrice(int $price): int
{
if (null === $this->currencyExchangeRate) {
throw new CurrencyException('No exchange rate for ' . $this->currency . ' to ' . Currency::DEFAULT_CURRENCY . ' has been provided.');
}
return round($price * $this->currencyExchangeRate);
}
/**
* @return null|string
*/
public function getAdditionalInfo(): ?string
{
return $this->additionalInfo;
}
/**
* @return null|string
*/
public function getCommodityCode(): ?string
{
return $this->commodityCode;
}
/**
* @return int|null
*/
public function getCostOfSales(): ?int
{
return $this->costOfSales;
}
/**
* @return DateTime
*/
public function getCreated(): DateTime
{
return $this->created;
}
/**
* @return string
*/
public function getCurrency(): string
{
return $this->currency;
}
/**
* @return float|null
*/
public function getCurrencyExchangeRate(): ?float
{
return $this->currencyExchangeRate;
}
/**
* @return array<SalesItemDelivery>
*/
public function getDeliveries(): array
{
return $this->deliveries;
}
/**
* @return string|null
*/
public function getDeliveryComments(): ?string
{
return $this->deliveryComments;
}
/**
* @return DateTime|null
*/
public function getDeliveryDateEstimate(): ?DateTime
{
return $this->deliveryDateEstimate;
}
/**
* @return float|null
*/
public function getDiscountPercentage(): ?float
{
return null !== $this->discountPercentage ? (float) $this->discountPercentage : null;
}
/**
* @return int|null
*/
public function getDiscountValue(): ?int
{
return $this->discountValue;
}
/**
* @return bool
*/
public function getExcludePrice(): bool
{
return $this->excludePrice;
}
/**
* @return DateTime|null
*/
public function getExportLicenseExpiryDate(): ?DateTime
{
return $this->exportLicenseExpiryDate;
}
/**
* @return DateTime|null
*/
public function getExportLicenseIssueDate(): ?DateTime
{
return $this->exportLicenseIssueDate;
}
/**
* @return null|string
*/
public function getExportLicenseNumberEu(): ?string
{
return $this->exportLicenseNumberEu;
}
/**
* @return null|string
*/
public function getExportLicenseNumberUs(): ?string
{
return $this->exportLicenseNumberUs;
}
/**
* @return int|null
*/
public function getExportLicenseQuantityEu(): ?int
{
return $this->exportLicenseQuantityEu;
}
/**
* @return int|null
*/
public function getExportLicenseQuantityUs(): ?int
{
return $this->exportLicenseQuantityUs;
}
/**
* @return null|string
*/
public function getLicenseSerialNumbers(): ?string
{
return $this->licenseSerialNumbers;
}
/**
* @return DateTime|null
*/
public function getFeaturesExpiryDate(): ?DateTime
{
return $this->featuresExpiryDate;
}
/**
* @return int|null
* @throws CurrencyException
*/
public function getFinvoiceRowVatExcludedAmount(): ?int
{
return $this->getPrice(false);
}
/**
* @return float|null
*
* @throws CurrencyException
*/
public function getGrossMargin(): ?float
{
if ($this->unitPrice === null || $this->costOfSales === null) {
return null;
}
$sales = $this->getPrice(true);
if ($sales > 0) {
return ($sales - ($this->quantity * $this->costOfSales)) / $sales;
}
return 0.0;
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return int|null
*/
public function getPciPosition(): ?int
{
if ($this->isPciSubItem()) {
return $this->position - ($this->position % SalesItem::POSITION_MULTIPLIER);
}
return null;
}
/**
* @return int
*/
public function getPosition(): int
{
return $this->position;
}
/**
* @return string
*/
public function getPositionDisplay(): string
{
if ($this->position >= SalesItem::POSITION_MULTIPLIER) {
$sub = $this->position % SalesItem::POSITION_MULTIPLIER;
$position = ($this->position - $sub) / SalesItem::POSITION_MULTIPLIER;
if ($sub > 0) {
return $position . '.' . $sub;
}
return $position;
}
return (string) $this->position;
}
/**
* @return int
*/
public function getPositionNormalized(): int
{
if ($this->position < self::POSITION_MULTIPLIER) {
return $this->position * self::POSITION_MULTIPLIER;
}
return $this->position;
}
/**
* @param bool $defaultCurrency
*
* @return int|null
*
* @throws CurrencyException
*/
public function getPrice(bool $defaultCurrency = false): ?int
{
if (null === $this->unitPrice) {
return null;
}
return $this->getPricePreDiscount($defaultCurrency) - $this->getPriceOfDiscount($defaultCurrency);
}
/**
* @param bool $defaultCurrency
*
* @return int|null
*
* @throws CurrencyException
*/
public function getTotalPrice(bool $defaultCurrency = false): ?int
{
return $this->getPrice($defaultCurrency);
}
/**
* @param bool $defaultCurrency
*
* @return int
*
* @throws CurrencyException
*/
public function getPriceOfDiscount(bool $defaultCurrency = false): int
{
if (null !== $this->discountValue) {
if ($defaultCurrency && !$this->isDefaultCurrency()) {
return $this->convertPrice($this->discountValue);
}
return $this->discountValue;
} elseif (null !== $this->discountPercentage) {
if (null !== $pricePreDiscount = $this->getPricePreDiscount($defaultCurrency)) {
return round($pricePreDiscount * $this->discountPercentage / 100);
}
}
return 0;
}
/**
* @param bool $defaultCurrency
*
* @return int|null
*
* @throws CurrencyException
*/
public function getPricePreDiscount(bool $defaultCurrency = false): ?int
{
if (null === $this->unitPrice) {
return null;
}
return $this->quantity * $this->getUnitPrice($defaultCurrency);
}
/**
* @param bool $defaultCurrency
*
* @return int|null
*
* @throws CurrencyException
*/
public function getPurchasePrice(bool $defaultCurrency = false): ?int
{
if (null !== $this->purchasePrice) {
if ($defaultCurrency && !$this->isDefaultCurrency()) {
return $this->convertPrice($this->purchasePrice);
}
}
return $this->purchasePrice;
}
/**
* @return int
*/
public function getQuantity(): int
{
return $this->quantity;
}
/**
* @return int|null
*/
public function getQuantityPerPci(): ?int
{
return $this->quantityPerPci;
}
/**
* @return DateTime|null
*/
public function getServiceEnd(): ?DateTime
{
return $this->serviceEnd;
}
/**
* @return null|string
*/
public function getServiceId(): ?string
{
return $this->serviceId;
}
/**
* @return DateTime|null
*/
public function getServiceStart(): ?DateTime
{
return $this->serviceStart;
}
/**
* @return Supplier|null
*/
public function getSupplier(): ?Supplier
{
return $this->supplier;
}
/**
* @return int
*/
public function getTotalQuantity(): int
{
return $this->totalQuantity;
}
/**
* @return int|null
*/
public function getTotalWeight(): ?int
{
if (null !== $this->weight) {
return $this->weight * $this->quantity;
}
return null;
}
/**
* @return string|null
*/
public function getUnit(): ?string
{
return $this->unit;
}
/**
* @return string[]
*/
public static function getUnitChoices(): array
{
return [
'pc',
'batch',
'set',
'lot',
'ea',
'day',
'year',
'metre',
'',
];
}
/**
* @param bool $defaultCurrency
*
* @return int|null
*
* @throws CurrencyException
*/
public function getUnitPrice(bool $defaultCurrency = false): ?int
{
if (null !== $this->unitPrice) {
if ($defaultCurrency && !$this->isDefaultCurrency()) {
return $this->convertPrice($this->unitPrice);
}
}
return $this->unitPrice;
}
/**
* @return null|string
*/
public function getVersionCode(): ?string
{
return $this->versionCode;
}
/**
* @return bool
*/
public function hasDiscount(): bool
{
return $this->discountValue !== null || $this->discountPercentage !== null;
}
/**
* @return bool|null
*/
public function isActive(): ?bool
{
if (null !== $isFuture = $this->isFuture()) {
if (null !== $isExpired = $this->isExpired()) {
return $isFuture === false && $isExpired === false;
}
}
return null;
}
/**
* @return bool
*/
public function isDefaultCurrency(): bool
{
return $this->currency == Currency::DEFAULT_CURRENCY;
}
/**
* @return bool
*/
public function isExcludePrice(): bool
{
return $this->excludePrice;
}
/**
* @return bool
*/
public function isInventory(): bool
{
return $this->inventory;
}
/**
* @return bool
*/
public function isLicenseVisible(): bool
{
return $this->licenseVisible;
}
/**
* @return bool|null
*/
public function isFuture(): ?bool
{
if ($this->isSupport() && null !== $serviceStart = $this->getServiceStart()) {
return $serviceStart->getTimestamp() > time();
}
return null;
}
/**
* @return bool|null
*/
public function isExpired(): ?bool
{
if ($this->isSupport() && null !== $serviceEnd = $this->getServiceEnd()) {
return $serviceEnd->getTimestamp() <= time();
}
return null;
}
/**
* @return bool
*/
public function isPciItem(): bool
{
return str_starts_with(strtolower($this->type), 'pci');
}
/**
* @return bool
*/
public function isPciExpandItem(): bool
{
return strtolower($this->type) === 'pci:expand';
}
/**
* @return bool
*/
public function isPciSubItem(): bool
{
return $this->position >= self::POSITION_MULTIPLIER && $this->position % self::POSITION_MULTIPLIER > 0;
}
/**
* @return bool
*/
public function isServicePeriodVisible(): bool
{
return $this->servicePeriodVisible;
}
/**
* @ORM\PostLoad
*/
public function postLoad(): void
{
// Set total quantity
$this->totalQuantity = $this->quantity;
// Unserialize deliveries
$this->deliveries = [];
if (is_array($this->deliveriesArray)) {
foreach ($this->deliveriesArray as $delivery) {
$this->deliveries[] = SalesItemDelivery::fromArray($delivery);
}
}
}
/**
* @ORM\PrePersist
*/
public function prePersist(): void
{
// Ensure there is a created time
$this->created = new DateTime();
}
/**
* @ORM\PreUpdate
*/
public function preUpdate(): void
{
// Serialize the deliveries
$this->deliveriesArray = null;
if (count($this->deliveries) > 0) {
$this->deliveriesArray = array_map(
function (SalesItemDelivery $delivery): array {
return $delivery->toArray();
},
$this->deliveries
);
}
}
/**
* @param string|null $additionalInfo
*/
public function setAdditionalInfo(?string $additionalInfo): void
{
$this->additionalInfo = $additionalInfo;
}
/**
* @param null|string $commodityCode
*/
public function setCommodityCode(?string $commodityCode): void
{
$this->commodityCode = $commodityCode;
}
/**
* @param int|null $costOfSales
*/
public function setCostOfSales(?int $costOfSales = null): void
{
$this->costOfSales = $costOfSales;
}
/**
* @param string $currency
*/
public function setCurrency(string $currency): void
{
$this->currency = $currency;
}
/**
* @param float|null $currencyExchangeRate
*/
public function setCurrencyExchangeRate(?float $currencyExchangeRate): void
{
$this->currencyExchangeRate = $currencyExchangeRate;
}
/**
* @param array<SalesItemDelivery> $deliveries
*/
public function setDeliveries(array $deliveries): void
{
$this->deliveries = [];
foreach ($deliveries as $delivery) {
if (null !== $delivery) {
$this->deliveries[] = $delivery;
}
}
}
/**
* @param string|null $deliveryComments
*/
public function setDeliveryComments(?string $deliveryComments): void
{
$this->deliveryComments = $deliveryComments;
}
/**
* @param DateTime|null $deliveryDateEstimate
*/
public function setDeliveryDateEstimate(?DateTime $deliveryDateEstimate): void
{
$this->deliveryDateEstimate = $deliveryDateEstimate;
}
/**
* @param float|null $discountPercentage
*/
public function setDiscountPercentage(?float $discountPercentage): void
{
$this->discountPercentage = null !== $discountPercentage ? number_format($discountPercentage, 2, '.', '') : null;
}
/**
* @param int|null $discountValue
*/
public function setDiscountValue(?int $discountValue): void
{
$this->discountValue = $discountValue;
}
/**
* @param bool $excludePrice
*/
public function setExcludePrice(bool $excludePrice): void
{
$this->excludePrice = $excludePrice;
}
/**
* @param DateTime|null $exportLicenseExpiryDate
*/
public function setExportLicenseExpiryDate(?DateTime $exportLicenseExpiryDate): void
{
$this->exportLicenseExpiryDate = $exportLicenseExpiryDate;
}
/**
* @param DateTime|null $exportLicenseIssueDate
*/
public function setExportLicenseIssueDate(?DateTime $exportLicenseIssueDate): void
{
$this->exportLicenseIssueDate = $exportLicenseIssueDate;
}
/**
* @param int|null $exportLicenseQuantityEu
*/
public function setExportLicenseQuantityEu(?int $exportLicenseQuantityEu): void
{
$this->exportLicenseQuantityEu = $exportLicenseQuantityEu;
}
/**
* @param int|null $exportLicenseQuantityUs
*/
public function setExportLicenseQuantityUs(?int $exportLicenseQuantityUs): void
{
$this->exportLicenseQuantityUs = $exportLicenseQuantityUs;
}
/**
* @param DateTime|null $featuresExpiryDate
*/
public function setFeaturesExpiryDate(?DateTime $featuresExpiryDate): void
{
$this->featuresExpiryDate = $featuresExpiryDate;
}
/**
* @param null|string $exportLicenseNumberEu
*/
public function setExportLicenseNumberEu(?string $exportLicenseNumberEu): void
{
$this->exportLicenseNumberEu = $exportLicenseNumberEu;
}
/**
* @param null|string $exportLicenseNumberUs
*/
public function setExportLicenseNumberUs(?string $exportLicenseNumberUs): void
{
$this->exportLicenseNumberUs = $exportLicenseNumberUs;
}
/**
* @param bool $inventory
*/
public function setInventory(bool $inventory): void
{
$this->inventory = $inventory;
}
/**
* @param null|string $licenseSerialNumbers
*/
public function setLicenseSerialNumbers(?string $licenseSerialNumbers): void
{
$this->licenseSerialNumbers = $licenseSerialNumbers;
}
/**
* @param bool $licenseVisible
*/
public function setLicenseVisible(bool $licenseVisible): void
{
$this->licenseVisible = $licenseVisible;
}
/**
* @param int $position
*/
public function setPosition(int $position): void
{
$this->position = $position;
}
/**
* @param int|null $purchasePrice
*/
public function setPurchasePrice(?int $purchasePrice): void
{
$this->purchasePrice = $purchasePrice;
}
/**
* @param int $quantity
*/
public function setQuantity(int $quantity): void
{
$this->quantity = $quantity;
}
/**
* @param int|null $quantityPerPci
*/
public function setQuantityPerPci(?int $quantityPerPci): void
{
$this->quantityPerPci = $quantityPerPci;
}
/**
* @param DateTime|null $serviceEnd
*/
public function setServiceEnd(?DateTime $serviceEnd): void
{
$this->serviceEnd = $serviceEnd;
}
/**
* @param string|null $serviceId
*/
public function setServiceId(?string $serviceId): void
{
$this->serviceId = $serviceId;
}
/**
* @param bool $servicePeriodVisible
*/
public function setServicePeriodVisible(bool $servicePeriodVisible): void
{
$this->servicePeriodVisible = $servicePeriodVisible;
}
/**
* @param DateTime|null $serviceStart
*/
public function setServiceStart(?DateTime $serviceStart): void
{
$this->serviceStart = $serviceStart;
}
/**
* @param Supplier|null $supplier
*/
public function setSupplier(?Supplier $supplier): void
{
$this->supplier = $supplier;
}
/**
* @param string|null $unit
*/
public function setUnit(?string $unit): void
{
$this->unit = $unit;
}
/**
* @param int|null $unitPrice
*/
public function setUnitPrice(?int $unitPrice): void
{
$this->unitPrice = $unitPrice;
}
/**
* @param string|null $versionCode
*/
public function setVersionCode(?string $versionCode): void
{
$this->versionCode = $versionCode;
}
/**
* @param Product $product
*/
public function updateProductData(Product $product): void
{
$this->setCode($product->getCode());
$this->setPlatform($product->getPlatform());
$this->setType($product->getType());
$this->setHeader($product->getHeader());
$this->setCustomsTariff($product->getCustomsTariff());
$this->setCommodityCode($product->getCustomsTariff());
$this->setCountryOfOrigin($product->getCountryOfOrigin());
$this->setExportLicense($product->isExportLicense());
$this->setLicenseFile($product->isLicenseFile());
$this->setFeatures($product->getFeatures());
}
/**
* @Assert\Callback
*
* @param ExecutionContextInterface $context
*/
public function validateDeliveriesQuantity(ExecutionContextInterface $context): void
{
$deliveredQuantity = 0;
foreach ($this->deliveries as $delivery) {
$deliveredQuantity += $delivery->getQuantity();
}
if ($deliveredQuantity > $this->quantity) {
$context->buildViolation('The quantity of delivered items cannot be greater than the ordered quantity.')
->atPath('deliveries')
->addViolation()
;
}
}
/**
* @Assert\Callback
*
* @param ExecutionContextInterface $context
*/
public function validateDiscountValue(ExecutionContextInterface $context): void
{
if ($this->discountValue !== null && $this->discountPercentage !== null) {
$context->buildViolation('You cannot define both discount value and percentage at the same time')
->atPath('discountValue')
->addViolation()
;
}
}
/**
* @Assert\Callback(
* groups={"Invoice"}
* )
*
* @param ExecutionContextInterface $context
*/
public function validateExportLicenseNumber(ExecutionContextInterface $context): void
{
if ($this->exportLicense) {
if (empty($this->exportLicenseNumberEu)) {
$context->buildViolation('You need to specify an export license number')
->atPath('exportLicenseNumberEu')
->addViolation()
;
}
if (empty($this->exportLicenseNumberUs)) {
$context->buildViolation('You need to specify an export license number')
->atPath('exportLicenseNumberUs')
->addViolation()
;
}
}
}
}