src/Service/PurchaseOrderService.php line 175

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\CompanyDocumentAddress;
  4. use App\Entity\DocumentVersionPurchaseOrder;
  5. use App\Entity\PurchaseOrder;
  6. use App\Entity\PurchasePriceGroup;
  7. use App\Entity\SalesCase;
  8. use App\Entity\SalesItem;
  9. use App\Entity\Supplier;
  10. use App\Exception\AccessDeniedException;
  11. use App\Exception\CurrencyException;
  12. use App\Exception\DeleteDocumentException;
  13. use App\Exception\DraftOrderConfirmationException;
  14. use App\Exception\InvalidArgumentException;
  15. use App\Exception\InventoryBalanceExceeded;
  16. use App\Exception\NoOrderConfirmationException;
  17. use App\Exception\NoSupplierException;
  18. use App\Model\Pdf\PurchaseOrderPdf;
  19. use App\Repository\PurchaseOrderRepository;
  20. use App\Security\Authorization\Voter\PurchaseOrderVoter;
  21. use Doctrine\ORM\NonUniqueResultException;
  22. use Exception;
  23. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  24. use Symfony\Component\Routing\RouterInterface;
  25. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  26. use Symfony\Contracts\Translation\TranslatorInterface;
  27. use Twig\Environment;
  28. class PurchaseOrderService
  29. {
  30.     /**
  31.      * @var AuthorizationCheckerInterface
  32.      */
  33.     protected AuthorizationCheckerInterface $authorizationChecker;
  34.     /**
  35.      * @var PriceGroupService
  36.      */
  37.     protected PriceGroupService $priceGroupService;
  38.     /**
  39.      * @var ProductService
  40.      */
  41.     protected ProductService $productService;
  42.     /**
  43.      * @var PurchaseOrderRepository
  44.      */
  45.     protected PurchaseOrderRepository $repository;
  46.     /**
  47.      * @var RouterInterface
  48.      */
  49.     protected RouterInterface $router;
  50.     /**
  51.      * @var SettingService
  52.      */
  53.     protected SettingService $settingService;
  54.     /**
  55.      * @var SupplierService
  56.      */
  57.     protected SupplierService $supplierService;
  58.     /**
  59.      * @var Environment
  60.      */
  61.     protected Environment $templating;
  62.     /**
  63.      * @var TranslatorInterface
  64.      */
  65.     protected TranslatorInterface $translator;
  66.     /**
  67.      * @var UserService
  68.      */
  69.     protected UserService $userService;
  70.     /**
  71.      * @param PurchaseOrderRepository $repository
  72.      * @param AuthorizationCheckerInterface $authorizationChecker
  73.      * @param RouterInterface $router
  74.      * @param Environment $templating
  75.      * @param ProductService $productService
  76.      * @param SupplierService $supplierService
  77.      * @param PriceGroupService $priceGroupService
  78.      * @param UserService $userService
  79.      * @param SettingService $settingService
  80.      * @param TranslatorInterface $translator
  81.      */
  82.     public function __construct(
  83.         PurchaseOrderRepository $repository,
  84.         AuthorizationCheckerInterface $authorizationChecker,
  85.         RouterInterface $router,
  86.         Environment $templating,
  87.         ProductService $productService,
  88.         SupplierService $supplierService,
  89.         PriceGroupService $priceGroupService,
  90.         UserService $userService,
  91.         SettingService $settingService,
  92.         TranslatorInterface $translator
  93.     ) {
  94.         $this->repository $repository;
  95.         $this->authorizationChecker $authorizationChecker;
  96.         $this->router $router;
  97.         $this->templating $templating;
  98.         $this->productService $productService;
  99.         $this->supplierService $supplierService;
  100.         $this->priceGroupService $priceGroupService;
  101.         $this->userService $userService;
  102.         $this->settingService $settingService;
  103.         $this->translator $translator;
  104.     }
  105.     /**
  106.      * @param PurchaseOrder $purchaseOrder
  107.      *
  108.      * @throws AccessDeniedException
  109.      */
  110.     public function assertCreate(PurchaseOrder $purchaseOrder): void
  111.     {
  112.         if (!$this->canCreate($purchaseOrder)) {
  113.             throw new AccessDeniedException('You are not allowed to create a purchase order.');
  114.         }
  115.     }
  116.     /**
  117.      * @param PurchaseOrder $purchaseOrder
  118.      *
  119.      * @throws AccessDeniedException
  120.      */
  121.     public function assertDelete(PurchaseOrder $purchaseOrder): void
  122.     {
  123.         if (!$this->canDelete($purchaseOrder)) {
  124.             throw new AccessDeniedException('You are not allowed to delete the purchase order.');
  125.         }
  126.     }
  127.     /**
  128.      * @param PurchaseOrder $purchaseOrder
  129.      *
  130.      * @throws InventoryBalanceExceeded
  131.      * @throws NonUniqueResultException
  132.      * @throws CurrencyException
  133.      */
  134.     public function assertInventoryBalanceNotExceeded(PurchaseOrder $purchaseOrder): void
  135.     {
  136.         if ($purchaseOrder->getSupplier()->isInventoryTransactions()) {
  137.             $inventoryItems $this->productService->getInventoryItems(nullnull$purchaseOrder);
  138.             foreach ($purchaseOrder->getSalesItems() as $item) {
  139.                 $code $item->getCode();
  140.                 $quantity $item->getQuantity();
  141.                 $balance 0;
  142.                 if (array_key_exists($code$inventoryItems)) {
  143.                     $balance $inventoryItems[$code]->getBalance();
  144.                 }
  145.                 if ($quantity $balance) {
  146.                     throw new InventoryBalanceExceeded('The quantity (' $quantity ') of item ' $code ' exceeds the current inventory balance (' $balance ').');
  147.                 }
  148.             }
  149.         }
  150.     }
  151.     /**
  152.      * @param PurchaseOrder $purchaseOrder
  153.      *
  154.      * @throws AccessDeniedException
  155.      */
  156.     public function assertRead(PurchaseOrder $purchaseOrder): void
  157.     {
  158.         if (!$this->canRead($purchaseOrder)) {
  159.             throw new AccessDeniedException('You are not allowed to view the purchase order.');
  160.         }
  161.     }
  162.     /**
  163.      * @param PurchaseOrder $purchaseOrder
  164.      *
  165.      * @throws AccessDeniedException
  166.      */
  167.     public function assertUpdate(PurchaseOrder $purchaseOrder): void
  168.     {
  169.         if (!$this->canUpdate($purchaseOrder)) {
  170.             throw new AccessDeniedException('You are not allowed to update the purchase order.');
  171.         }
  172.     }
  173.     /**
  174.      * @param PurchaseOrder $purchaseOrder
  175.      *
  176.      * @return bool
  177.      */
  178.     public function canCreate(PurchaseOrder $purchaseOrder): bool
  179.     {
  180.         return $this->authorizationChecker->isGranted(
  181.             PurchaseOrderVoter::CREATE,
  182.             $purchaseOrder
  183.         );
  184.     }
  185.     /**
  186.      * @param PurchaseOrder $purchaseOrder
  187.      *
  188.      * @return bool
  189.      */
  190.     public function canDelete(PurchaseOrder $purchaseOrder): bool
  191.     {
  192.         return $this->authorizationChecker->isGranted(
  193.             PurchaseOrderVoter::DELETE,
  194.             $purchaseOrder
  195.         );
  196.     }
  197.     /**
  198.      * @param PurchaseOrder $purchaseOrder
  199.      *
  200.      * @return bool
  201.      */
  202.     public function canRead(PurchaseOrder $purchaseOrder): bool
  203.     {
  204.         return $this->authorizationChecker->isGranted(
  205.             PurchaseOrderVoter::READ,
  206.             $purchaseOrder
  207.         );
  208.     }
  209.     /**
  210.      * @param PurchaseOrder $purchaseOrder
  211.      *
  212.      * @return bool
  213.      */
  214.     public function canUpdate(PurchaseOrder $purchaseOrder): bool
  215.     {
  216.         return $this->authorizationChecker->isGranted(
  217.             PurchaseOrderVoter::UPDATE,
  218.             $purchaseOrder
  219.         );
  220.     }
  221.     /**
  222.      * @param PurchaseOrder $purchaseOrder
  223.      */
  224.     public function create(PurchaseOrder $purchaseOrder): void
  225.     {
  226.         $purchaseOrder->addCreateLogEntry(
  227.             $this->userService->getCurrentUser()
  228.         );
  229.         $this->repository->save($purchaseOrder);
  230.     }
  231.     /**
  232.      * @param PurchaseOrder $purchaseOrder
  233.      *
  234.      * @throws DeleteDocumentException
  235.      */
  236.     public function delete(PurchaseOrder $purchaseOrder): void
  237.     {
  238.         if (!$purchaseOrder->isDraft()) {
  239.             throw new DeleteDocumentException('Only draft purchase orders can be deleted.');
  240.         }
  241.         $this->repository->delete($purchaseOrder);
  242.     }
  243.     /**
  244.      * @param PurchaseOrder $purchaseOrder
  245.      *
  246.      * @return string
  247.      *
  248.      * @throws Exception
  249.      */
  250.     public function generatePdf(PurchaseOrder $purchaseOrder): string
  251.     {
  252.         $pdf = new PurchaseOrderPdf(
  253.             $purchaseOrder,
  254.             $this->translator,
  255.             $this->settingService->getCompanyDetails()
  256.         );
  257.         return $pdf->getContent();
  258.     }
  259.     /**
  260.      * @param PurchaseOrder $purchaseOrder
  261.      *
  262.      * @return DocumentVersionPurchaseOrder
  263.      *
  264.      * @throws InvalidArgumentException
  265.      */
  266.     public function generateVersion(PurchaseOrder $purchaseOrder): DocumentVersionPurchaseOrder
  267.     {
  268.         $documentVersion = new DocumentVersionPurchaseOrder(
  269.             $this->generatePdf($purchaseOrder),
  270.             $purchaseOrder
  271.         );
  272.         $purchaseOrder->addDocumentVersion($documentVersion);
  273.         $this->update($purchaseOrder);
  274.         return $documentVersion;
  275.     }
  276.     /**
  277.      * @param SalesCase $salesCase
  278.      * @param Supplier $supplier
  279.      * @param array<SalesItem> $salesItems
  280.      *
  281.      * @return PurchaseOrder
  282.      *
  283.      * @throws NoOrderConfirmationException
  284.      * @throws DraftOrderConfirmationException
  285.      * @throws InventoryBalanceExceeded
  286.      * @throws CurrencyException
  287.      * @throws NonUniqueResultException
  288.      */
  289.     public function getNew(SalesCase $salesCaseSupplier $supplier, array $salesItems = []): PurchaseOrder
  290.     {
  291.         $purchaseOrder = new PurchaseOrder($salesCase$supplier);
  292.         if ($salesCase->getType() != SalesCase::TYPE_PURCHASE_ORDER) {
  293.             if (null === $orderConfirmation $salesCase->getOrderConfirmation()) {
  294.                 throw new NoOrderConfirmationException("The sales case '" $salesCase->getNumber() . "' has no order confirmation.");
  295.             }
  296.             if ($orderConfirmation->isDraft()) {
  297.                 throw new DraftOrderConfirmationException("The order confirmation of the sales case '" $salesCase->getNumber() . "' is still in draft status.");
  298.             }
  299.             /**
  300.              * Set values automatically from the order confirmation
  301.              */
  302.             // Contact person
  303.             if (null !== $contactPerson $orderConfirmation->getContactPerson()) {
  304.                 $purchaseOrder->setContactPerson($contactPerson);
  305.             }
  306.             // Customer reference number
  307.             $purchaseOrder->setCustomerReferenceNumber(
  308.                 $orderConfirmation->getCustomerReferenceNumber()
  309.             );
  310.             // Delivery address
  311.             if (null !== $deliveryAddress $orderConfirmation->getDeliveryAddress()) {
  312.                 $purchaseOrder->setDeliveryAddress(
  313.                     clone $deliveryAddress
  314.                 );
  315.             }
  316.             // End customer
  317.             if (null !== $endCustomer $orderConfirmation->getEndCustomer()) {
  318.                 $purchaseOrder->setEndCustomer(
  319.                     clone $endCustomer
  320.                 );
  321.             }
  322.             // Shipping marks
  323.             $purchaseOrder->setShippingMarks(
  324.                 $orderConfirmation->getShippingMarks()
  325.             );
  326.             // SOC number
  327.             $purchaseOrder->setSocNumber(
  328.                 $orderConfirmation->getNumber()
  329.             );
  330.         }
  331.         // Invoicing address
  332.         if (null !== $inventory $this->supplierService->getInventory()) {
  333.             $purchaseOrder->setInvoicingAddress(
  334.                 CompanyDocumentAddress::create($inventory)
  335.             );
  336.         }
  337.         // Contact person
  338.         if ($purchaseOrder->getContactPerson() === null) {
  339.             $purchaseOrder->setContactPerson($this->userService->getCurrentUser()->getName());
  340.         }
  341.         // Terms of delivery
  342.         $purchaseOrder->setTermsOfDelivery(
  343.             $supplier->getTermsOfDeliveryForDocument()
  344.         );
  345.         // Terms of payment
  346.         foreach ($supplier->getTermsOfPaymentRows() as $row) {
  347.             $purchaseOrder->addTermsOfPaymentRow(clone $row);
  348.         }
  349.         // Transportation
  350.         $purchaseOrder->setTransportation(
  351.             $supplier->getTransportation()
  352.         );
  353.         $useInventoryPrices false;
  354.         $inventoryItems = [];
  355.         if ($supplier->isInventoryTransactions()) {
  356.             $useInventoryPrices true;
  357.             $inventoryItems $this->productService->getInventoryItems();
  358.         }
  359.         $salesItemPosition 1;
  360.         foreach ($salesItems as $salesItem) {
  361.             $salesItem = clone $salesItem;
  362.             $salesItem->setPosition($salesItemPosition++);
  363.             $salesItem->setAdditionalInfo(null);
  364.             $salesItem->setDiscountPercentage(null);
  365.             $salesItem->setDiscountValue(null);
  366.             $salesItem->setUnitPrice(0);
  367.             $salesItem->setPurchasePrice(null);
  368.             if ($useInventoryPrices) {
  369.                 if (!array_key_exists($salesItem->getCode(), $inventoryItems)) {
  370.                     throw new InventoryBalanceExceeded('The sales item ' $salesItem->getCode() . ' is not available in the inventory.');
  371.                 }
  372.                 $salesItem->setUnitPrice($inventoryItems[$salesItem->getCode()]->getMeanUnitPrice());
  373.             } else {
  374.                 if (null !== $product $this->productService->getByCode($salesItem->getCode())) {
  375.                     $versions $product->getProductVersions();
  376.                     if (count($versions) == 1) {
  377.                         $version $versions[0];
  378.                         $salesItem->setVersionCode($version->getCode());
  379.                         if (null !== $price $version->getPrice()) {
  380.                             $salesItem->setUnitPrice($price->getPrice());
  381.                             $salesItem->setPurchasePrice($price->getPrice());
  382.                         }
  383.                     }
  384.                 }
  385.             }
  386.             $purchaseOrder->addSalesItem($salesItem);
  387.         }
  388.         return $purchaseOrder;
  389.     }
  390.     /**
  391.      * @param SalesCase $salesCase
  392.      *
  393.      * @return PurchaseOrder[]
  394.      *
  395.      * @throws NoOrderConfirmationException
  396.      * @throws DraftOrderConfirmationException
  397.      * @throws NoSupplierException
  398.      */
  399.     public function getNewAll(SalesCase $salesCase): array
  400.     {
  401.         if (null === $orderConfirmation $salesCase->getOrderConfirmation()) {
  402.             throw new NoOrderConfirmationException("The sales case '" $salesCase->getNumber() . "' has no order confirmation.");
  403.         }
  404.         if ($orderConfirmation->isDraft()) {
  405.             throw new DraftOrderConfirmationException("The order confirmation of the sales case '" $salesCase->getNumber() . "' is still in draft status.");
  406.         }
  407.         $salesItemGroups = [];
  408.         foreach ($orderConfirmation->getSalesItems() as $item) {
  409.             if (null === $supplier $item->getSupplier()) {
  410.                 throw new NoSupplierException('The sales item at position ' $item->getPosition() . ' has no supplier defined.');
  411.             }
  412.             if (!isset($salesItemGroups[$supplier->getId()])) {
  413.                 $salesItemGroups[$supplier->getId()] = [
  414.                     'supplier' => $supplier,
  415.                     'items'    => [],
  416.                 ];
  417.             }
  418.             $newItem = clone $item;
  419.             $salesItemGroups[$supplier->getId()]['items'][] = $newItem;
  420.         }
  421.         $purchaseOrders = [];
  422.         foreach ($salesItemGroups as $group) {
  423.             $purchaseOrders[] = $this->getNew(
  424.                 $salesCase,
  425.                 $group['supplier'],
  426.                 $group['items']
  427.             );
  428.         }
  429.         return $purchaseOrders;
  430.     }
  431.     /**
  432.      * @return PurchaseOrder[]
  433.      */
  434.     public function getOpen(): array
  435.     {
  436.         return $this->repository->findOpen();
  437.     }
  438.     /**
  439.      * @return array
  440.      *
  441.      * @throws CurrencyException
  442.      */
  443.     public function getPriceUpdates(): array
  444.     {
  445.         $updates = [];
  446.         $purchasePriceGroup $this->priceGroupService->getPurchasePriceGroup();
  447.         $openPurchaseOrders $this->getOpen();
  448.         foreach ($openPurchaseOrders as $purchaseOrder) {
  449.             $salesItemUpdates $this->getPriceUpdatesForPurchaseOrder($purchaseOrder$purchasePriceGroup);
  450.             if (count($salesItemUpdates) > 0) {
  451.                 $updates[] = [
  452.                     'purchaseOrder' => $purchaseOrder,
  453.                     'salesItems'    => $salesItemUpdates,
  454.                 ];
  455.             }
  456.         }
  457.         return $updates;
  458.     }
  459.     /**
  460.      * @param PurchaseOrder           $purchaseOrder
  461.      * @param PurchasePriceGroup|null $purchasePriceGroup
  462.      *
  463.      * @return array
  464.      *
  465.      * @throws CurrencyException
  466.      */
  467.     public function getPriceUpdatesForPurchaseOrder(
  468.         PurchaseOrder $purchaseOrder,
  469.         PurchasePriceGroup $purchasePriceGroup null
  470.     ): array {
  471.         $updates = [];
  472.         // Price updates are shown only for purchase orders in non-completed sales cases
  473.         if (! $purchaseOrder->getSalesCase()->isCompleted()) {
  474.             // Check for new versions
  475.             foreach ($purchaseOrder->getSalesItems() as $salesItem) {
  476.                 if (null !== $versionCode $salesItem->getVersionCode()) {
  477.                     $product $this->productService->getByCode($salesItem->getCode());
  478.                     if ($product !== null) {
  479.                         if (null !== $currentProductVersion $product->getCurrentProductVersion()) {
  480.                             if ($currentProductVersion->getCode() !== $versionCode) {
  481.                                 $priceValue null;
  482.                                 if (null !== $price $currentProductVersion->getPrice()) {
  483.                                     $priceValue $price->getPrice();
  484.                                 }
  485.                                 $updates[$salesItem->getPosition()] = [
  486.                                     'salesItem' => $salesItem,
  487.                                     'price'     => $priceValue,
  488.                                     'version'   => $currentProductVersion->getCode(),
  489.                                 ];
  490.                             }
  491.                         }
  492.                     }
  493.                 }
  494.             }
  495.             // Check for new purchase prices
  496.             if ($purchasePriceGroup === null) {
  497.                 $purchasePriceGroup $this->priceGroupService->getPurchasePriceGroup();
  498.             }
  499.             if ($purchasePriceGroup !== null) {
  500.                 if (null !== $currentPriceList $purchasePriceGroup->getCurrentPriceList()) {
  501.                     foreach ($purchaseOrder->getSalesItems() as $salesItem) {
  502.                         $versionCode $salesItem->getVersionCode();
  503.                         foreach ($currentPriceList->getPrices() as $price) {
  504.                             if (null !== $productVersion $price->getProductVersion()) {
  505.                                 if ($productVersion->getCode() == $versionCode && $salesItem->getUnitPrice() != ($newPrice $price->getPrice())) {
  506.                                     if (isset($updates[$salesItem->getPosition()])) {
  507.                                         $updates[$salesItem->getPosition()]['price'] = $newPrice;
  508.                                     } else {
  509.                                         $updates[$salesItem->getPosition()] = [
  510.                                             'salesItem' => $salesItem,
  511.                                             'price'     => $newPrice,
  512.                                             'version'   => null,
  513.                                         ];
  514.                                     }
  515.                                 }
  516.                             }
  517.                         }
  518.                     }
  519.                 }
  520.             }
  521.         }
  522.         return $updates;
  523.     }
  524.     /**
  525.      * @param PurchaseOrder $purchaseOrder
  526.      */
  527.     public function update(PurchaseOrder $purchaseOrder): void
  528.     {
  529.         $purchaseOrder->addUpdateLogEntry(
  530.             $this->userService->getCurrentUser()
  531.         );
  532.         // Set platform, type and header to all new sales items
  533.         foreach ($purchaseOrder->getSalesItems() as $item) {
  534.             if ($item->getId() === null) {
  535.                 if (null !== $product $this->productService->getByCode($item->getCode())) {
  536.                     $item->setPlatform($product->getPlatform());
  537.                     $item->setType($product->getType());
  538.                     $item->setHeader($product->getHeader());
  539.                 }
  540.             }
  541.         }
  542.         $this->repository->save($purchaseOrder);
  543.     }
  544.     /**
  545.      * @param PurchaseOrder $purchaseOrder
  546.      * @param string        $status
  547.      *
  548.      * @throws InvalidArgumentException
  549.      */
  550.     public function updateStatus(PurchaseOrder $purchaseOrderstring $status): void
  551.     {
  552.         $originalStatus $purchaseOrder->getOriginalStatus();
  553.         $purchaseOrder->setStatus($status);
  554.         if ($originalStatus == PurchaseOrder::STATUS_DRAFT && $purchaseOrder->getOrderNumber() === null) {
  555.             $purchaseOrder->setOrderNumber(
  556.                 $this->repository->findNextOrderNumber()
  557.             );
  558.         }
  559.         $purchaseOrder->addStatusLogEntry(
  560.             $this->userService->getCurrentUser(),
  561.             'Changed status ' $originalStatus ' to ' $purchaseOrder->getStatus()
  562.         );
  563.         $this->repository->save($purchaseOrder);
  564.     }
  565. }