<?php
namespace App\Service;
use App\Entity\CompanyDocumentAddress;
use App\Entity\DocumentVersionPurchaseOrder;
use App\Entity\PurchaseOrder;
use App\Entity\PurchasePriceGroup;
use App\Entity\SalesCase;
use App\Entity\SalesItem;
use App\Entity\Supplier;
use App\Exception\AccessDeniedException;
use App\Exception\CurrencyException;
use App\Exception\DeleteDocumentException;
use App\Exception\DraftOrderConfirmationException;
use App\Exception\InvalidArgumentException;
use App\Exception\InventoryBalanceExceeded;
use App\Exception\NoOrderConfirmationException;
use App\Exception\NoSupplierException;
use App\Model\Pdf\PurchaseOrderPdf;
use App\Repository\PurchaseOrderRepository;
use App\Security\Authorization\Voter\PurchaseOrderVoter;
use Doctrine\ORM\NonUniqueResultException;
use Exception;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
class PurchaseOrderService
{
/**
* @var AuthorizationCheckerInterface
*/
protected AuthorizationCheckerInterface $authorizationChecker;
/**
* @var PriceGroupService
*/
protected PriceGroupService $priceGroupService;
/**
* @var ProductService
*/
protected ProductService $productService;
/**
* @var PurchaseOrderRepository
*/
protected PurchaseOrderRepository $repository;
/**
* @var RouterInterface
*/
protected RouterInterface $router;
/**
* @var SettingService
*/
protected SettingService $settingService;
/**
* @var SupplierService
*/
protected SupplierService $supplierService;
/**
* @var Environment
*/
protected Environment $templating;
/**
* @var TranslatorInterface
*/
protected TranslatorInterface $translator;
/**
* @var UserService
*/
protected UserService $userService;
/**
* @param PurchaseOrderRepository $repository
* @param AuthorizationCheckerInterface $authorizationChecker
* @param RouterInterface $router
* @param Environment $templating
* @param ProductService $productService
* @param SupplierService $supplierService
* @param PriceGroupService $priceGroupService
* @param UserService $userService
* @param SettingService $settingService
* @param TranslatorInterface $translator
*/
public function __construct(
PurchaseOrderRepository $repository,
AuthorizationCheckerInterface $authorizationChecker,
RouterInterface $router,
Environment $templating,
ProductService $productService,
SupplierService $supplierService,
PriceGroupService $priceGroupService,
UserService $userService,
SettingService $settingService,
TranslatorInterface $translator
) {
$this->repository = $repository;
$this->authorizationChecker = $authorizationChecker;
$this->router = $router;
$this->templating = $templating;
$this->productService = $productService;
$this->supplierService = $supplierService;
$this->priceGroupService = $priceGroupService;
$this->userService = $userService;
$this->settingService = $settingService;
$this->translator = $translator;
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws AccessDeniedException
*/
public function assertCreate(PurchaseOrder $purchaseOrder): void
{
if (!$this->canCreate($purchaseOrder)) {
throw new AccessDeniedException('You are not allowed to create a purchase order.');
}
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws AccessDeniedException
*/
public function assertDelete(PurchaseOrder $purchaseOrder): void
{
if (!$this->canDelete($purchaseOrder)) {
throw new AccessDeniedException('You are not allowed to delete the purchase order.');
}
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws InventoryBalanceExceeded
* @throws NonUniqueResultException
* @throws CurrencyException
*/
public function assertInventoryBalanceNotExceeded(PurchaseOrder $purchaseOrder): void
{
if ($purchaseOrder->getSupplier()->isInventoryTransactions()) {
$inventoryItems = $this->productService->getInventoryItems(null, null, $purchaseOrder);
foreach ($purchaseOrder->getSalesItems() as $item) {
$code = $item->getCode();
$quantity = $item->getQuantity();
$balance = 0;
if (array_key_exists($code, $inventoryItems)) {
$balance = $inventoryItems[$code]->getBalance();
}
if ($quantity > $balance) {
throw new InventoryBalanceExceeded('The quantity (' . $quantity . ') of item ' . $code . ' exceeds the current inventory balance (' . $balance . ').');
}
}
}
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws AccessDeniedException
*/
public function assertRead(PurchaseOrder $purchaseOrder): void
{
if (!$this->canRead($purchaseOrder)) {
throw new AccessDeniedException('You are not allowed to view the purchase order.');
}
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws AccessDeniedException
*/
public function assertUpdate(PurchaseOrder $purchaseOrder): void
{
if (!$this->canUpdate($purchaseOrder)) {
throw new AccessDeniedException('You are not allowed to update the purchase order.');
}
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return bool
*/
public function canCreate(PurchaseOrder $purchaseOrder): bool
{
return $this->authorizationChecker->isGranted(
PurchaseOrderVoter::CREATE,
$purchaseOrder
);
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return bool
*/
public function canDelete(PurchaseOrder $purchaseOrder): bool
{
return $this->authorizationChecker->isGranted(
PurchaseOrderVoter::DELETE,
$purchaseOrder
);
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return bool
*/
public function canRead(PurchaseOrder $purchaseOrder): bool
{
return $this->authorizationChecker->isGranted(
PurchaseOrderVoter::READ,
$purchaseOrder
);
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return bool
*/
public function canUpdate(PurchaseOrder $purchaseOrder): bool
{
return $this->authorizationChecker->isGranted(
PurchaseOrderVoter::UPDATE,
$purchaseOrder
);
}
/**
* @param PurchaseOrder $purchaseOrder
*/
public function create(PurchaseOrder $purchaseOrder): void
{
$purchaseOrder->addCreateLogEntry(
$this->userService->getCurrentUser()
);
$this->repository->save($purchaseOrder);
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @throws DeleteDocumentException
*/
public function delete(PurchaseOrder $purchaseOrder): void
{
if (!$purchaseOrder->isDraft()) {
throw new DeleteDocumentException('Only draft purchase orders can be deleted.');
}
$this->repository->delete($purchaseOrder);
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return string
*
* @throws Exception
*/
public function generatePdf(PurchaseOrder $purchaseOrder): string
{
$pdf = new PurchaseOrderPdf(
$purchaseOrder,
$this->translator,
$this->settingService->getCompanyDetails()
);
return $pdf->getContent();
}
/**
* @param PurchaseOrder $purchaseOrder
*
* @return DocumentVersionPurchaseOrder
*
* @throws InvalidArgumentException
*/
public function generateVersion(PurchaseOrder $purchaseOrder): DocumentVersionPurchaseOrder
{
$documentVersion = new DocumentVersionPurchaseOrder(
$this->generatePdf($purchaseOrder),
$purchaseOrder
);
$purchaseOrder->addDocumentVersion($documentVersion);
$this->update($purchaseOrder);
return $documentVersion;
}
/**
* @param SalesCase $salesCase
* @param Supplier $supplier
* @param array<SalesItem> $salesItems
*
* @return PurchaseOrder
*
* @throws NoOrderConfirmationException
* @throws DraftOrderConfirmationException
* @throws InventoryBalanceExceeded
* @throws CurrencyException
* @throws NonUniqueResultException
*/
public function getNew(SalesCase $salesCase, Supplier $supplier, array $salesItems = []): PurchaseOrder
{
$purchaseOrder = new PurchaseOrder($salesCase, $supplier);
if ($salesCase->getType() != SalesCase::TYPE_PURCHASE_ORDER) {
if (null === $orderConfirmation = $salesCase->getOrderConfirmation()) {
throw new NoOrderConfirmationException("The sales case '" . $salesCase->getNumber() . "' has no order confirmation.");
}
if ($orderConfirmation->isDraft()) {
throw new DraftOrderConfirmationException("The order confirmation of the sales case '" . $salesCase->getNumber() . "' is still in draft status.");
}
/**
* Set values automatically from the order confirmation
*/
// Contact person
if (null !== $contactPerson = $orderConfirmation->getContactPerson()) {
$purchaseOrder->setContactPerson($contactPerson);
}
// Customer reference number
$purchaseOrder->setCustomerReferenceNumber(
$orderConfirmation->getCustomerReferenceNumber()
);
// Delivery address
if (null !== $deliveryAddress = $orderConfirmation->getDeliveryAddress()) {
$purchaseOrder->setDeliveryAddress(
clone $deliveryAddress
);
}
// End customer
if (null !== $endCustomer = $orderConfirmation->getEndCustomer()) {
$purchaseOrder->setEndCustomer(
clone $endCustomer
);
}
// Shipping marks
$purchaseOrder->setShippingMarks(
$orderConfirmation->getShippingMarks()
);
// SOC number
$purchaseOrder->setSocNumber(
$orderConfirmation->getNumber()
);
}
// Invoicing address
if (null !== $inventory = $this->supplierService->getInventory()) {
$purchaseOrder->setInvoicingAddress(
CompanyDocumentAddress::create($inventory)
);
}
// Contact person
if ($purchaseOrder->getContactPerson() === null) {
$purchaseOrder->setContactPerson($this->userService->getCurrentUser()->getName());
}
// Terms of delivery
$purchaseOrder->setTermsOfDelivery(
$supplier->getTermsOfDeliveryForDocument()
);
// Terms of payment
foreach ($supplier->getTermsOfPaymentRows() as $row) {
$purchaseOrder->addTermsOfPaymentRow(clone $row);
}
// Transportation
$purchaseOrder->setTransportation(
$supplier->getTransportation()
);
$useInventoryPrices = false;
$inventoryItems = [];
if ($supplier->isInventoryTransactions()) {
$useInventoryPrices = true;
$inventoryItems = $this->productService->getInventoryItems();
}
$salesItemPosition = 1;
foreach ($salesItems as $salesItem) {
$salesItem = clone $salesItem;
$salesItem->setPosition($salesItemPosition++);
$salesItem->setAdditionalInfo(null);
$salesItem->setDiscountPercentage(null);
$salesItem->setDiscountValue(null);
$salesItem->setUnitPrice(0);
$salesItem->setPurchasePrice(null);
if ($useInventoryPrices) {
if (!array_key_exists($salesItem->getCode(), $inventoryItems)) {
throw new InventoryBalanceExceeded('The sales item ' . $salesItem->getCode() . ' is not available in the inventory.');
}
$salesItem->setUnitPrice($inventoryItems[$salesItem->getCode()]->getMeanUnitPrice());
} else {
if (null !== $product = $this->productService->getByCode($salesItem->getCode())) {
$versions = $product->getProductVersions();
if (count($versions) == 1) {
$version = $versions[0];
$salesItem->setVersionCode($version->getCode());
if (null !== $price = $version->getPrice()) {
$salesItem->setUnitPrice($price->getPrice());
$salesItem->setPurchasePrice($price->getPrice());
}
}
}
}
$purchaseOrder->addSalesItem($salesItem);
}
return $purchaseOrder;
}
/**
* @param SalesCase $salesCase
*
* @return PurchaseOrder[]
*
* @throws NoOrderConfirmationException
* @throws DraftOrderConfirmationException
* @throws NoSupplierException
*/
public function getNewAll(SalesCase $salesCase): array
{
if (null === $orderConfirmation = $salesCase->getOrderConfirmation()) {
throw new NoOrderConfirmationException("The sales case '" . $salesCase->getNumber() . "' has no order confirmation.");
}
if ($orderConfirmation->isDraft()) {
throw new DraftOrderConfirmationException("The order confirmation of the sales case '" . $salesCase->getNumber() . "' is still in draft status.");
}
$salesItemGroups = [];
foreach ($orderConfirmation->getSalesItems() as $item) {
if (null === $supplier = $item->getSupplier()) {
throw new NoSupplierException('The sales item at position ' . $item->getPosition() . ' has no supplier defined.');
}
if (!isset($salesItemGroups[$supplier->getId()])) {
$salesItemGroups[$supplier->getId()] = [
'supplier' => $supplier,
'items' => [],
];
}
$newItem = clone $item;
$salesItemGroups[$supplier->getId()]['items'][] = $newItem;
}
$purchaseOrders = [];
foreach ($salesItemGroups as $group) {
$purchaseOrders[] = $this->getNew(
$salesCase,
$group['supplier'],
$group['items']
);
}
return $purchaseOrders;
}
/**
* @return PurchaseOrder[]
*/
public function getOpen(): array
{
return $this->repository->findOpen();
}
/**
* @return array
*
* @throws CurrencyException
*/
public function getPriceUpdates(): array
{
$updates = [];
$purchasePriceGroup = $this->priceGroupService->getPurchasePriceGroup();
$openPurchaseOrders = $this->getOpen();
foreach ($openPurchaseOrders as $purchaseOrder) {
$salesItemUpdates = $this->getPriceUpdatesForPurchaseOrder($purchaseOrder, $purchasePriceGroup);
if (count($salesItemUpdates) > 0) {
$updates[] = [
'purchaseOrder' => $purchaseOrder,
'salesItems' => $salesItemUpdates,
];
}
}
return $updates;
}
/**
* @param PurchaseOrder $purchaseOrder
* @param PurchasePriceGroup|null $purchasePriceGroup
*
* @return array
*
* @throws CurrencyException
*/
public function getPriceUpdatesForPurchaseOrder(
PurchaseOrder $purchaseOrder,
PurchasePriceGroup $purchasePriceGroup = null
): array {
$updates = [];
// Price updates are shown only for purchase orders in non-completed sales cases
if (! $purchaseOrder->getSalesCase()->isCompleted()) {
// Check for new versions
foreach ($purchaseOrder->getSalesItems() as $salesItem) {
if (null !== $versionCode = $salesItem->getVersionCode()) {
$product = $this->productService->getByCode($salesItem->getCode());
if ($product !== null) {
if (null !== $currentProductVersion = $product->getCurrentProductVersion()) {
if ($currentProductVersion->getCode() !== $versionCode) {
$priceValue = null;
if (null !== $price = $currentProductVersion->getPrice()) {
$priceValue = $price->getPrice();
}
$updates[$salesItem->getPosition()] = [
'salesItem' => $salesItem,
'price' => $priceValue,
'version' => $currentProductVersion->getCode(),
];
}
}
}
}
}
// Check for new purchase prices
if ($purchasePriceGroup === null) {
$purchasePriceGroup = $this->priceGroupService->getPurchasePriceGroup();
}
if ($purchasePriceGroup !== null) {
if (null !== $currentPriceList = $purchasePriceGroup->getCurrentPriceList()) {
foreach ($purchaseOrder->getSalesItems() as $salesItem) {
$versionCode = $salesItem->getVersionCode();
foreach ($currentPriceList->getPrices() as $price) {
if (null !== $productVersion = $price->getProductVersion()) {
if ($productVersion->getCode() == $versionCode && $salesItem->getUnitPrice() != ($newPrice = $price->getPrice())) {
if (isset($updates[$salesItem->getPosition()])) {
$updates[$salesItem->getPosition()]['price'] = $newPrice;
} else {
$updates[$salesItem->getPosition()] = [
'salesItem' => $salesItem,
'price' => $newPrice,
'version' => null,
];
}
}
}
}
}
}
}
}
return $updates;
}
/**
* @param PurchaseOrder $purchaseOrder
*/
public function update(PurchaseOrder $purchaseOrder): void
{
$purchaseOrder->addUpdateLogEntry(
$this->userService->getCurrentUser()
);
// Set platform, type and header to all new sales items
foreach ($purchaseOrder->getSalesItems() as $item) {
if ($item->getId() === null) {
if (null !== $product = $this->productService->getByCode($item->getCode())) {
$item->setPlatform($product->getPlatform());
$item->setType($product->getType());
$item->setHeader($product->getHeader());
}
}
}
$this->repository->save($purchaseOrder);
}
/**
* @param PurchaseOrder $purchaseOrder
* @param string $status
*
* @throws InvalidArgumentException
*/
public function updateStatus(PurchaseOrder $purchaseOrder, string $status): void
{
$originalStatus = $purchaseOrder->getOriginalStatus();
$purchaseOrder->setStatus($status);
if ($originalStatus == PurchaseOrder::STATUS_DRAFT && $purchaseOrder->getOrderNumber() === null) {
$purchaseOrder->setOrderNumber(
$this->repository->findNextOrderNumber()
);
}
$purchaseOrder->addStatusLogEntry(
$this->userService->getCurrentUser(),
'Changed status ' . $originalStatus . ' to ' . $purchaseOrder->getStatus()
);
$this->repository->save($purchaseOrder);
}
}