<?php
namespace App\Entity;
use App\Exception\InvalidArgumentException;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity(
* repositoryClass="App\Repository\PurchaseOrderRepository"
* )
* @ORM\Table(
* name="purchase_order",
* options={"collate"="utf8_swedish_ci"}
* )
* @ORM\HasLifecycleCallbacks
*/
class PurchaseOrder implements EntityInterface, LoggableInterface, SalesDocumentInterface, SalesDocumentDiscountInterface
{
use DocumentTrait;
use LoggableTrait;
use SalesItemListTrait;
const NUMBER_PREFIX = 'PO';
const ORDER_NUMBER_PREFIX = 2;
const STATUS_DRAFT = 'draft';
const STATUS_SENT = 'sent';
const STATUS_REJECTED = 'rejected';
/**
* @ORM\Id
* @ORM\Column(
* type="integer"
* )
* @ORM\GeneratedValue(
* strategy="AUTO"
* )
*
* @var int|null
*/
protected ?int $id = null;
/**
* @ORM\Column(
* type="string",
* length=3
* )
* @Assert\Choice(
* callback={"\App\Model\Currency", "getCodeChoices"}
* )
*
* @var string
*/
protected string $currency;
/**
* @ORM\OneToOne(
* targetEntity="CompanyDocumentAddress",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinColumn(
* name="delivery_address_id"
* )
*
* @Assert\Valid
*
* @var CompanyDocumentAddress|null
*/
protected ?CompanyDocumentAddress $deliveryAddress = null;
/**
* @ORM\Column(
* name="delivery_date",
* type="date",
* nullable=true
* )
*
* @var DateTime|null
*/
protected ?DateTime $deliveryDate = null;
/**
* @ORM\OneToMany(
* targetEntity="DocumentVersionPurchaseOrder",
* mappedBy="purchaseOrder",
* cascade={"persist","remove"}
* )
* @ORM\OrderBy({"created" = "DESC"})
*
* @var Collection<DocumentVersionPurchaseOrder>
*/
protected Collection $documentVersions;
/**
* @ORM\OneToOne(
* targetEntity="DocumentEndCustomer",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinColumn(
* name="end_customer_id"
* )
*
* @Assert\Valid
*
* @var DocumentEndCustomer|null
*/
protected ?DocumentEndCustomer $endCustomer = null;
/**
* @ORM\OneToOne(
* targetEntity="CompanyDocumentAddress",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinColumn(
* name="invoicing_address_id"
* )
*
* @Assert\Valid
*
* @var CompanyDocumentAddress|null
*/
protected ?CompanyDocumentAddress $invoicingAddress = null;
/**
* @ORM\ManyToMany(
* targetEntity="LogEntry",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinTable(
* name="purchase_order_log_entry",
* joinColumns={
* @ORM\JoinColumn(
* name="purchase_order_id",
* referencedColumnName="id",
* onDelete="cascade"
* )
* },
* inverseJoinColumns={
* @ORM\JoinColumn(
* name="log_entry_id",
* referencedColumnName="id",
* unique=true,
* onDelete="cascade"
* )
* }
* )
* @ORM\OrderBy({"time" = "ASC"})
*
* @Assert\Valid
*
* @var Collection<LogEntry>
*/
protected $logEntries;
/**
* @ORM\Column(
* type="string",
* length=5000,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $notes = null;
/**
* @ORM\Column(
* name="order_number",
* type="integer",
* nullable=true
* )
*
* @var int|null
*/
protected ?int $orderNumber = null;
/**
* @ORM\ManyToOne(
* targetEntity="SalesCase",
* inversedBy="purchaseOrders"
* )
* @ORM\JoinColumn(
* name="sales_case_id",
* referencedColumnName="id",
* onDelete="cascade"
* )
* @Assert\NotNull
*
* @var SalesCase
*/
protected SalesCase $salesCase;
/**
* @ORM\ManyToMany(
* targetEntity="SalesItem",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinTable(
* name="purchase_order_sales_item",
* joinColumns={
* @ORM\JoinColumn(
* name="purchase_order_id",
* referencedColumnName="id",
* onDelete="cascade"
* )
* },
* inverseJoinColumns={
* @ORM\JoinColumn(
* name="sales_item_id",
* referencedColumnName="id",
* unique=true,
* onDelete="cascade"
* )
* }
* )
* @ORM\OrderBy({"position" = "ASC"})
*
* @Assert\Valid
*
* @var Collection<SalesItem>
*/
protected Collection $salesItems;
/**
* @ORM\Column(
* name="shipping_marks",
* type="string",
* length=500,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $shippingMarks = null;
/**
* @ORM\Column(
* name="soc_number",
* type="string",
* length=50,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $socNumber = null;
/**
* @ORM\ManyToOne(
* targetEntity="Supplier",
* inversedBy="purchaseOrders"
* )
* @ORM\JoinColumn(
* name="supplier_id",
* referencedColumnName="id",
* onDelete="SET NULL"
* )
*
* @Assert\NotNull
*
* @var Supplier
*/
protected Supplier $supplier;
/**
* @ORM\OneToOne(
* targetEntity="CompanyDocumentAddress",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinColumn(
* name="supplier_address_id"
* )
* @Assert\Valid
*
* @var CompanyDocumentAddress|null
*/
protected ?CompanyDocumentAddress $supplierAddress = null;
/**
* @ORM\Column(
* name="terms_of_delivery",
* type="string",
* length=500,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $termsOfDelivery = null;
/**
* @ORM\ManyToMany(
* targetEntity="TermsOfPaymentRow",
* cascade={"persist","remove"},
* orphanRemoval=true
* )
* @ORM\JoinTable(
* name="purchase_order_terms_of_payment_row",
* joinColumns={
* @ORM\JoinColumn(
* name="purchase_order_id",
* referencedColumnName="id",
* onDelete="cascade"
* )
* },
* inverseJoinColumns={
* @ORM\JoinColumn(
* name="terms_of_payment_row_id",
* referencedColumnName="id",
* unique=true,
* onDelete="cascade"
* )
* }
* )
* @Assert\Valid
*
* @var Collection<TermsOfPaymentRow>
*/
protected Collection $termsOfPaymentRows;
/**
* @ORM\Column(
* type="text",
* nullable=true
* )
*
* @var string|null
*/
protected ?string $text = null;
/**
* @ORM\Column(
* type="string",
* length=500,
* nullable=true
* )
*
* @var string|null
*/
protected ?string $transportation = null;
/**
* @param SalesCase $salesCase
* @param Supplier $supplier
*/
public function __construct(SalesCase $salesCase, Supplier $supplier)
{
$this->salesCase = $salesCase;
$this->setSupplier($supplier);
$this->documentVersions = new ArrayCollection();
$this->logEntries = new ArrayCollection();
$this->salesItems = new ArrayCollection();
$this->termsOfPaymentRows = new ArrayCollection();
}
public function __clone(): void
{
$this->id = null;
// Clone objects
if (null !== $this->deliveryAddress) {
$this->deliveryAddress = clone $this->deliveryAddress;
}
if (null !== $this->endCustomer) {
$this->endCustomer = clone $this->endCustomer;
}
if (null !== $this->invoicingAddress) {
$this->invoicingAddress = clone $this->invoicingAddress;
}
if (null !== $this->supplierAddress) {
$this->supplierAddress = clone $this->supplierAddress;
}
// Clone collections
$this->documentVersions = new ArrayCollection();
$this->logEntries = new ArrayCollection();
$salesItems = new ArrayCollection();
foreach ($this->salesItems as $salesItem) {
$salesItems->add(clone $salesItem);
}
$this->salesItems = $salesItems;
$termsOfPaymentRows = new ArrayCollection();
foreach ($this->termsOfPaymentRows as $termsOfPaymentRow) {
$termsOfPaymentRows->add(clone $termsOfPaymentRow);
}
$this->termsOfPaymentRows = $termsOfPaymentRows;
}
/**
* @param DocumentVersionPurchaseOrder $documentVersion
*
* @throws InvalidArgumentException
*/
public function addDocumentVersion(DocumentVersionPurchaseOrder $documentVersion): void
{
if ($documentVersion->getPurchaseOrder() !== $this) {
throw new InvalidArgumentException('Document version has mismatching purchase order document.');
}
$this->documentVersions->add($documentVersion);
}
/**
* @param TermsOfPaymentRow $row
*/
public function addTermsOfPaymentRow(TermsOfPaymentRow $row): void
{
if ($this->termsOfPaymentRows === null) {
$this->termsOfPaymentRows = new ArrayCollection();
}
$this->termsOfPaymentRows->add($row);
}
/**
* @return string
*/
public function getCurrency(): string
{
return $this->currency;
}
/**
* @return CompanyDocumentAddress|null
*/
public function getDeliveryAddress(): ?CompanyDocumentAddress
{
return $this->deliveryAddress;
}
/**
* @return DateTime|null
*/
public function getDeliveryDate(): ?DateTime
{
return $this->deliveryDate;
}
/**
* @return Collection<DocumentVersionPurchaseOrder>
*/
public function getDocumentVersions(): Collection
{
return $this->documentVersions;
}
/**
* @return DocumentEndCustomer|null
*/
public function getEndCustomer(): ?DocumentEndCustomer
{
return $this->endCustomer;
}
/**
* @return float
*
* @throws Exception
*/
public function getGrossMargin(): float
{
throw new Exception('Purchase orders do not have gross margins.');
}
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return CompanyDocumentAddress|null
*/
public function getInvoicingAddress(): ?CompanyDocumentAddress
{
return $this->invoicingAddress;
}
/**
* @return null|string
*/
public function getNotes(): ?string
{
return $this->notes;
}
/**
* @return string
*/
public function getNumber(): string
{
return self::NUMBER_PREFIX . str_pad($this->salesCase->getId(), 6, '0', STR_PAD_LEFT)
. '-'
. ($this->isDraft() ? 'DRAFT' : self::ORDER_NUMBER_PREFIX . str_pad($this->getOrderNumber(), 5, '0', STR_PAD_LEFT));
}
/**
* @return int|null
*/
public function getOrderNumber(): ?int
{
return $this->orderNumber;
}
/**
* @return string
*/
public function getPreviewFileName(): string
{
return 'Purchase Order - ' . $this->getNumber() . ' - Preview ' . date('Y-m-d') . '.pdf';
}
/**
* @return SalesCase
*/
public function getSalesCase(): SalesCase
{
return $this->salesCase;
}
/**
* @return null|string
*/
public function getShippingMarks(): ?string
{
return $this->shippingMarks;
}
/**
* @return null|string
*/
public function getSocNumber(): ?string
{
return $this->socNumber;
}
/**
* @return array<string>
*/
public static function getStatusChoices(): array
{
return [
self::STATUS_DRAFT,
self::STATUS_SENT,
self::STATUS_REJECTED,
];
}
/**
* @return Supplier
*/
public function getSupplier(): Supplier
{
return $this->supplier;
}
/**
* @return CompanyDocumentAddress|null
*/
public function getSupplierAddress(): ?CompanyDocumentAddress
{
return $this->supplierAddress;
}
/**
* @return null|string
*/
public function getTermsOfDelivery(): ?string
{
return $this->termsOfDelivery;
}
/**
* @return Collection<TermsOfPaymentRow>
*/
public function getTermsOfPaymentRows(): Collection
{
return $this->termsOfPaymentRows;
}
/**
* @return null|string
*/
public function getText(): ?string
{
return $this->text;
}
/**
* @param bool $defaultCurrency
*
* @return int
*
* @throws Exception
*/
public function getTotalPurchaseValue(bool $defaultCurrency = false): int
{
$total = 0;
$noPurchasePrice = [];
foreach ($this->salesItems as $salesItem) {
if (null !== $purchasePrice = $salesItem->getPurchasePrice($defaultCurrency)) {
$temp = $purchasePrice * $salesItem->getQuantity();
if (null !== $discountValue = $salesItem->getDiscountValue()) {
$temp -= $discountValue;
} elseif (null !== $discountPercentage = $salesItem->getDiscountPercentage()) {
$temp -= round($temp * $discountPercentage / 100);
}
$total += $temp;
} elseif ($this->getSupplier()->isGrossMarginPurchasesZero()) {
$total += 0;
} else {
$noPurchasePrice[] = $salesItem->getPositionDisplay();
}
}
if (count($noPurchasePrice) > 0) {
throw new Exception('Purchase order ' . $this->getNumber() . ', positions ' . implode(', ', $noPurchasePrice) . ' have no purchase price.');
}
return $total;
}
/**
* @param bool $defaultCurrency
*
* @return array
*/
public function getTotalPurchaseValueDisplay(bool $defaultCurrency = false): array
{
$value = null;
$error = null;
try {
$value = $this->getTotalPurchaseValue($defaultCurrency);
} catch (Exception $e) {
$error = $e->getMessage();
}
return [$value, $error];
}
/**
* @return null|string
*/
public function getTransportation(): ?string
{
return $this->transportation;
}
/**
* @return bool
*/
public function isDraft(): bool
{
return $this->getStatus() == self::STATUS_DRAFT;
}
/**
* @return bool
*/
public function isRejected(): bool
{
return $this->getStatus() == self::STATUS_REJECTED;
}
/**
* @return bool
*/
public function isSent(): bool
{
return $this->getStatus() == self::STATUS_SENT;
}
/**
* @ORM\PostLoad
*/
public function postLoadSetCurrency(): void
{
foreach ($this->salesItems as $salesItem) {
$salesItem->setCurrency($this->getCurrency());
if (null !== $exchangeRate = $this->getCurrencyExchangeRate()) {
$salesItem->setCurrencyExchangeRate($exchangeRate->getRate());
}
}
}
/**
* @param TermsOfPaymentRow $row
*/
public function removeTermsOfPaymentRow(TermsOfPaymentRow $row): void
{
if ($this->termsOfPaymentRows !== null) {
$this->termsOfPaymentRows->removeElement($row);
}
}
/**
* @param CompanyDocumentAddress|null $deliveryAddress
*/
public function setDeliveryAddress(?CompanyDocumentAddress $deliveryAddress): void
{
$this->deliveryAddress = $deliveryAddress;
}
/**
* @param DateTime|null $deliveryDate
*/
public function setDeliveryDate(?DateTime $deliveryDate): void
{
$this->deliveryDate = $deliveryDate;
}
/**
* @param DocumentEndCustomer|null $endCustomer
*/
public function setEndCustomer(?DocumentEndCustomer $endCustomer): void
{
$this->endCustomer = $endCustomer;
}
/**
* @param CompanyDocumentAddress|null $invoicingAddress
*/
public function setInvoicingAddress(?CompanyDocumentAddress $invoicingAddress): void
{
$this->invoicingAddress = $invoicingAddress;
}
/**
* @param string|null $notes
*/
public function setNotes(?string $notes): void
{
$this->notes = $notes;
}
/**
* @param int|null $orderNumber
*/
public function setOrderNumber(?int $orderNumber): void
{
$this->orderNumber = $orderNumber;
}
/**
* @param string|null $shippingMarks
*/
public function setShippingMarks(?string $shippingMarks): void
{
$this->shippingMarks = $shippingMarks;
}
/**
* @param string|null $socNumber
*/
public function setSocNumber(?string $socNumber): void
{
$this->socNumber = $socNumber;
}
/**
* @param Supplier $supplier
*/
private function setSupplier(Supplier $supplier): void
{
$this->supplier = $supplier;
$this->currency = $supplier->getCurrency();
$this->setSupplierAddress(
CompanyDocumentAddress::create($supplier)
);
}
/**
* @param CompanyDocumentAddress|null $supplierAddress
*/
public function setSupplierAddress(?CompanyDocumentAddress $supplierAddress): void
{
$this->supplierAddress = $supplierAddress;
}
/**
* @param string|null $termsOfDelivery
*/
public function setTermsOfDelivery(?string $termsOfDelivery): void
{
$this->termsOfDelivery = $termsOfDelivery;
}
/**
* @param string|null $text
*/
public function setText(?string $text): void
{
$this->text = $text;
}
/**
* @param string|null $transportation
*/
public function setTransportation(?string $transportation): void
{
$this->transportation = $transportation;
}
/**
* @Assert\Callback
*
* @param ExecutionContextInterface $context
*/
public function validateCurrencyExchangeRate(ExecutionContextInterface $context): void
{
if ($this->currencyExchangeRate !== null && $this->currencyExchangeRateDate === null) {
$context->buildViolation('Please define the date for the exchange rate')
->atPath('currencyExchangeRate.date')
->addViolation()
;
}
}
/**
* @Assert\Callback
*
* @param ExecutionContextInterface $context
*/
public function validateVat(ExecutionContextInterface $context): void
{
if ($this->id === null && $this->vat === null) {
$context->buildViolation('Please define VAT')
->atPath('vat')
->addViolation()
;
}
}
}