src/Entity/SalesCase.php line 26

Open in your IDE?
  1. <?php
  2. namespace App\Entity;
  3. use App\Exception\CurrencyException;
  4. use App\Exception\InvalidArgumentException;
  5. use App\Exception\NoOrderConfirmationException;
  6. use App\Model\Currency;
  7. use DateTime;
  8. use Doctrine\Common\Collections\ArrayCollection;
  9. use Doctrine\Common\Collections\Collection;
  10. use Doctrine\ORM\Mapping as ORM;
  11. use Symfony\Component\Validator\Constraints as Assert;
  12. use Symfony\Component\Validator\Context\ExecutionContextInterface;
  13. /**
  14.  * @ORM\Entity(
  15.  *     repositoryClass="App\Repository\SalesCaseRepository"
  16.  * )
  17.  * @ORM\Table(
  18.  *     name="sales_case",
  19.  *     options={"collate"="utf8_swedish_ci"}
  20.  * )
  21.  * @ORM\HasLifecycleCallbacks
  22.  */
  23. class SalesCase implements EntityInterface
  24. {
  25.     const NUMBER_PREFIX 'C';
  26.     const TYPE_OFFER              'offer';
  27.     const TYPE_ORDER_CONFIRMATION 'order_confirmation';
  28.     const TYPE_PURCHASE_ORDER     'purchase_order';
  29.     /**
  30.      * @ORM\Id
  31.      * @ORM\Column(
  32.      *     type="integer"
  33.      * )
  34.      * @ORM\GeneratedValue(
  35.      *     strategy="AUTO"
  36.      * )
  37.      *
  38.      * @var int|null
  39.      */
  40.     protected ?int $id null;
  41.     /**
  42.      * @ORM\OneToMany(
  43.      *     targetEntity="SalesCaseAttachment",
  44.      *     mappedBy="salesCase",
  45.      *     cascade={"persist","remove"},
  46.      *     orphanRemoval=true
  47.      * )
  48.      * @ORM\OrderBy({"created" = "DESC"})
  49.      *
  50.      * @var Collection<SalesCaseAttachment>
  51.      */
  52.     protected Collection $attachments;
  53.     /**
  54.      * @ORM\ManyToOne(
  55.      *     targetEntity="Company",
  56.      *     inversedBy="salesCases"
  57.      * )
  58.      * @ORM\JoinColumn(
  59.      *     name="company_id",
  60.      *     referencedColumnName="id"
  61.      * )
  62.      * @Assert\NotNull
  63.      *
  64.      * NOTE! There are sales cases in the database with no company set. For this null values need to be supported
  65.      * for already persisted sales cases. Remove in future.
  66.      *
  67.      * @var Company|null
  68.      */
  69.     protected ?Company $company null;
  70.     /**
  71.      * @ORM\Column(
  72.      *     type="string",
  73.      *     length=3
  74.      * )
  75.      * @Assert\Choice(
  76.      *     callback={"\App\Model\Currency", "getCodeChoices"}
  77.      * )
  78.      *
  79.      * @var string
  80.      */
  81.     protected string $currency Currency::DEFAULT_CURRENCY;
  82.     /**
  83.      * This is used internally for checking if the value of currency is being changed and validation.
  84.      *
  85.      * @var string
  86.      */
  87.     protected $currencyOriginal;
  88.     /**
  89.      * @ORM\Column(
  90.      *     type="string",
  91.      *     length=2000,
  92.      *     nullable=true
  93.      * )
  94.      *
  95.      * @var string|null
  96.      */
  97.     protected ?string $description null;
  98.     /**
  99.      * @ORM\OneToMany(
  100.      *     targetEntity="AbstractInvoice",
  101.      *     mappedBy="salesCase",
  102.      *     cascade={"persist","remove"}
  103.      * )
  104.      *
  105.      * @var Collection<AbstractInvoice>
  106.      */
  107.     protected Collection $invoices;
  108.     /**
  109.      * @ORM\OneToMany(
  110.      *     targetEntity="Notification",
  111.      *     mappedBy="salesCase",
  112.      *     cascade={"persist","remove"},
  113.      *     orphanRemoval=true
  114.      * )
  115.      * @ORM\OrderBy({"time" = "ASC"})
  116.      * @Assert\Valid
  117.      *
  118.      * @var Collection<Notification>
  119.      */
  120.     protected Collection $notifications;
  121.     /**
  122.      * @ORM\OneToOne(
  123.      *     targetEntity="Offer",
  124.      *     mappedBy="salesCase",
  125.      *     cascade={"persist","remove"},
  126.      *     fetch="EXTRA_LAZY"
  127.      * )
  128.      *
  129.      * @var Offer|null
  130.      */
  131.     protected ?Offer $offer null;
  132.     /**
  133.      * @ORM\Column(
  134.      *     type="datetime"
  135.      * )
  136.      *
  137.      * @var DateTime
  138.      */
  139.     protected DateTime $opened;
  140.     /**
  141.      * @ORM\OneToOne(
  142.      *     targetEntity="OrderConfirmation",
  143.      *     mappedBy="salesCase",
  144.      *     cascade={"persist","remove"}
  145.      * )
  146.      *
  147.      * @var OrderConfirmation|null
  148.      */
  149.     protected ?OrderConfirmation $orderConfirmation null;
  150.     /**
  151.      * @ORM\OneToMany(
  152.      *     targetEntity="PackingList",
  153.      *     mappedBy="salesCase",
  154.      *     cascade={"persist","remove"}
  155.      * )
  156.      *
  157.      * @var Collection<PackingList>
  158.      */
  159.     protected Collection $packingLists;
  160.     /**
  161.      * @ORM\OneToMany(
  162.      *     targetEntity="PurchaseOrder",
  163.      *     mappedBy="salesCase",
  164.      *     cascade={"persist","remove"}
  165.      * )
  166.      *
  167.      * @var Collection<PurchaseOrder>
  168.      */
  169.     protected Collection $purchaseOrders;
  170.     /**
  171.      * @ORM\Column(
  172.      *     type="string",
  173.      *     length=20
  174.      * )
  175.      * @Assert\Choice(
  176.      *     callback="getTypeChoices"
  177.      * )
  178.      *
  179.      * @var string
  180.      */
  181.     protected string $type self::TYPE_OFFER;
  182.     /**
  183.      * @param Company $company
  184.      * @param string $type
  185.      */
  186.     public function __construct(Company $companystring $type self::TYPE_OFFER)
  187.     {
  188.         $this->setCompany($company);
  189.         $this->setType($type);
  190.         $this->attachments = new ArrayCollection();
  191.         $this->invoices = new ArrayCollection();
  192.         $this->notifications = new ArrayCollection();
  193.         $this->packingLists = new ArrayCollection();
  194.         $this->purchaseOrders = new ArrayCollection();
  195.         $this->opened = new DateTime();
  196.     }
  197.     /**
  198.      * @param SalesCaseAttachment $attachment
  199.      *
  200.      * @throws InvalidArgumentException
  201.      */
  202.     public function addAttachment(SalesCaseAttachment $attachment): void
  203.     {
  204.         if ($attachment->getSalesCase() !== $this) {
  205.             throw new InvalidArgumentException('Invalid sales case for attachment');
  206.         }
  207.         $this->attachments->add($attachment);
  208.     }
  209.     /**
  210.      * @param Notification $notification
  211.      */
  212.     public function addNotification(Notification $notification): void
  213.     {
  214.         $notification->setSalesCase($this);
  215.         $this->notifications->add($notification);
  216.     }
  217.     /**
  218.      * @return SalesCaseAttachment[]|Collection
  219.      */
  220.     public function getAttachments()
  221.     {
  222.         return $this->attachments;
  223.     }
  224.     /**
  225.      * @return BankAccount|null
  226.      */
  227.     public function getBankAccount(): ?BankAccount
  228.     {
  229.         return $this->company->getBankAccount();
  230.     }
  231.     /**
  232.      * @return Company|null
  233.      */
  234.     public function getCompany(): ?Company
  235.     {
  236.         return $this->company;
  237.     }
  238.     /**
  239.      * @param bool $defaultCurrency
  240.      *
  241.      * @return int
  242.      *
  243.      * @throws CurrencyException
  244.      */
  245.     public function getCostOfSales(bool $defaultCurrency false): int
  246.     {
  247.         $total 0;
  248.         foreach ($this->getPurchaseOrders() as $purchaseOrder) {
  249.             if ($purchaseOrder->isSent()) {
  250.                 $total += $purchaseOrder->getValueNet($defaultCurrency);
  251.             }
  252.         }
  253.         return $total;
  254.     }
  255.     /**
  256.      * @return string
  257.      */
  258.     public function getCurrency(): string
  259.     {
  260.         return $this->currency;
  261.     }
  262.     /**
  263.      * @return null|string
  264.      */
  265.     public function getDescription(): ?string
  266.     {
  267.         return $this->description;
  268.     }
  269.     /**
  270.      * @return float
  271.      *
  272.      * @throws NoOrderConfirmationException
  273.      * @throws CurrencyException
  274.      */
  275.     public function getGrossMargin(): float
  276.     {
  277.         if (null === $orderConfirmation $this->getOrderConfirmation()) {
  278.             throw new NoOrderConfirmationException('The sales case does not have an order confirmation.');
  279.         }
  280.         $sales $orderConfirmation->getValueNet(false);
  281.         if ($sales == 0) {
  282.             return 0.0;
  283.         }
  284.         return ($sales $this->getCostOfSales(false)) / $sales;
  285.     }
  286.     /**
  287.      * @return int|null
  288.      */
  289.     public function getId(): ?int
  290.     {
  291.         return $this->id;
  292.     }
  293.     /**
  294.      * @return Collection<AbstractInvoice>
  295.      */
  296.     public function getInvoices(): Collection
  297.     {
  298.         return $this->invoices;
  299.     }
  300.     /**
  301.      * @param bool $excludeDrafts
  302.      *
  303.      * @return Collection<CreditInvoice>
  304.      */
  305.     public function getInvoicesOfTypeCreditInvoice(bool $excludeDrafts false): Collection
  306.     {
  307.         return $this->invoices->filter(
  308.             function ($invoice) use ($excludeDrafts) {
  309.                 /* @var AbstractInvoice $invoice */
  310.                 if ($excludeDrafts && $invoice->isDraft()) {
  311.                     return false;
  312.                 }
  313.                 return $invoice instanceof CreditInvoice;
  314.             }
  315.         );
  316.     }
  317.     /**
  318.      * @param bool $excludeDrafts
  319.      *
  320.      * @return Collection<DownPaymentInvoice>
  321.      */
  322.     public function getInvoicesOfTypeDownPaymentInvoice(bool $excludeDrafts false): Collection
  323.     {
  324.         return $this->invoices->filter(
  325.             function ($invoice) use ($excludeDrafts) {
  326.                 /* @var AbstractInvoice $invoice */
  327.                 if ($excludeDrafts && $invoice->isDraft()) {
  328.                     return false;
  329.                 }
  330.                 return $invoice instanceof DownPaymentInvoice;
  331.             }
  332.         );
  333.     }
  334.     /**
  335.      * @param bool $excludeDrafts
  336.      *
  337.      * @return Collection<Invoice>
  338.      */
  339.     public function getInvoicesOfTypeInvoice(bool $excludeDrafts false): Collection
  340.     {
  341.         return $this->invoices->filter(
  342.             function ($invoice) use ($excludeDrafts) {
  343.                 /* @var AbstractInvoice $invoice */
  344.                 if ($excludeDrafts && $invoice->isDraft()) {
  345.                     return false;
  346.                 }
  347.                 return $invoice instanceof Invoice;
  348.             }
  349.         );
  350.     }
  351.     /**
  352.      * @param bool $excludeDrafts
  353.      *
  354.      * @return Collection<ProformaInvoice>
  355.      */
  356.     public function getInvoicesOfTypeProformaInvoice(bool $excludeDrafts false): Collection
  357.     {
  358.         return $this->invoices->filter(
  359.             function ($invoice) use ($excludeDrafts) {
  360.                 /* @var AbstractInvoice $invoice */
  361.                 if ($excludeDrafts && $invoice->isDraft()) {
  362.                     return false;
  363.                 }
  364.                 return $invoice instanceof ProformaInvoice;
  365.             }
  366.         );
  367.     }
  368.     /**
  369.      * @return CreditInvoice
  370.      */
  371.     public function getNewCreditInvoice(): CreditInvoice
  372.     {
  373.         return new CreditInvoice($this);
  374.     }
  375.     /**
  376.      * @return DownPaymentInvoice
  377.      */
  378.     public function getNewDownPaymentInvoice(): DownPaymentInvoice
  379.     {
  380.         return new DownPaymentInvoice($this);
  381.     }
  382.     /**
  383.      * @return Invoice
  384.      */
  385.     public function getNewInvoice(): Invoice
  386.     {
  387.         return new Invoice($this);
  388.     }
  389.     /**
  390.      * @return ProformaInvoice
  391.      */
  392.     public function getNewProformaInvoice(): ProformaInvoice
  393.     {
  394.         return new ProformaInvoice($this);
  395.     }
  396.     /**
  397.      * @return PackingList
  398.      */
  399.     public function getNewPackingList(): PackingList
  400.     {
  401.         return new PackingList($this);
  402.     }
  403.     /**
  404.      * @return PurchaseOrder
  405.      */
  406.     public function getNewPurchaseOrder(): PurchaseOrder
  407.     {
  408.         return new PurchaseOrder($this, new Supplier(''));
  409.     }
  410.     /**
  411.      * @return Notification|null
  412.      */
  413.     public function getNextNotification(): ?Notification
  414.     {
  415.         foreach ($this->getNotifications() as $notification) {
  416.             if ($notification->getTime()->getTimestamp() > time()) {
  417.                 return $notification;
  418.             }
  419.         }
  420.         return null;
  421.     }
  422.     /**
  423.      * @return Collection<Notification>
  424.      */
  425.     public function getNotifications(): Collection
  426.     {
  427.         return $this->notifications;
  428.     }
  429.     /**
  430.      * @return string
  431.      */
  432.     public function getNumber(): string
  433.     {
  434.         return self::NUMBER_PREFIX str_pad($this->id6'0'STR_PAD_LEFT);
  435.     }
  436.     /**
  437.      * @return Offer|null
  438.      */
  439.     public function getOffer(): ?Offer
  440.     {
  441.         return $this->offer;
  442.     }
  443.     /**
  444.      * @return DateTime
  445.      */
  446.     public function getOpened(): DateTime
  447.     {
  448.         return $this->opened;
  449.     }
  450.     /**
  451.      * @return OrderConfirmation|null
  452.      */
  453.     public function getOrderConfirmation(): ?OrderConfirmation
  454.     {
  455.         return $this->orderConfirmation;
  456.     }
  457.     /**
  458.      * @return Collection<PackingList>
  459.      */
  460.     public function getPackingLists(): Collection
  461.     {
  462.         return $this->packingLists;
  463.     }
  464.     /**
  465.      * @return Collection<PurchaseOrder>
  466.      */
  467.     public function getPurchaseOrders(): Collection
  468.     {
  469.         return $this->purchaseOrders;
  470.     }
  471.     /**
  472.      * @param bool $defaultCurrency
  473.      *
  474.      * @return int
  475.      *
  476.      * @throws NoOrderConfirmationException
  477.      */
  478.     public function getRemainingInvoiceValue(bool $defaultCurrency false): int
  479.     {
  480.         if ($this->orderConfirmation === null) {
  481.             throw new NoOrderConfirmationException('The sales case has no order confirmation');
  482.         }
  483.         $totalInvoiced 0;
  484.         foreach ($this->getInvoicesOfTypeInvoice() as $invoice) {
  485.             if (!$invoice->isDraft()) {
  486.                 $totalInvoiced += $invoice->getValueNet($defaultCurrency);
  487.             }
  488.         }
  489.         foreach ($this->getInvoicesOfTypeCreditInvoice() as $invoice) {
  490.             if (!$invoice->isDraft()) {
  491.                 $totalInvoiced -= $invoice->getValueNet($defaultCurrency);
  492.             }
  493.         }
  494.         return $this->orderConfirmation->getValueNet($defaultCurrency) - $totalInvoiced;
  495.     }
  496.     /**
  497.      * @return string
  498.      */
  499.     public function getStatus(): string
  500.     {
  501.         if ($this->type == self::TYPE_OFFER) {
  502.             if ($this->offer === null) {
  503.                 return 'draft';
  504.             } elseif ($this->orderConfirmation === null) {
  505.                 return 'offer_' $this->offer->getStatus();
  506.             } else {
  507.                 return 'oc_' $this->orderConfirmation->getStatus();
  508.             }
  509.         } elseif ($this->type == self::TYPE_ORDER_CONFIRMATION) {
  510.             if ($this->orderConfirmation === null) {
  511.                 return 'draft';
  512.             } else {
  513.                 return 'oc_' $this->orderConfirmation->getStatus();
  514.             }
  515.         } elseif ($this->type == self::TYPE_PURCHASE_ORDER) {
  516.             if (count($this->purchaseOrders) == 0) {
  517.                 return 'draft';
  518.             } else {
  519.                 return 'po';
  520.             }
  521.         }
  522.         return '';
  523.     }
  524.     /**
  525.      * @return string[]
  526.      */
  527.     public static function getStatusChoices(): array
  528.     {
  529.         return [
  530.             'draft',
  531.             'offer_draft',
  532.             'offer_sent',
  533.             'offer_rejected',
  534.             'oc_draft',
  535.             'oc_not_confirmed',
  536.             'oc_preliminary_confirmed',
  537.             'oc_confirmed',
  538.             'oc_partially_delivered',
  539.             'oc_equipment_delivered',
  540.             'oc_delivered',
  541.             'oc_completed',
  542.             'oc_risk_order',
  543.             'oc_rejected',
  544.             'po',
  545.         ];
  546.     }
  547.     /**
  548.      * @return string
  549.      */
  550.     public function getType(): string
  551.     {
  552.         return $this->type;
  553.     }
  554.     /**
  555.      * @return string[]
  556.      */
  557.     public static function getTypeChoices(): array
  558.     {
  559.         return [
  560.             self::TYPE_OFFER,
  561.             self::TYPE_ORDER_CONFIRMATION,
  562.             self::TYPE_PURCHASE_ORDER,
  563.         ];
  564.     }
  565.     /**
  566.      * @return bool
  567.      */
  568.     public function isCompleted(): bool
  569.     {
  570.         if ($this->type == self::TYPE_PURCHASE_ORDER) {
  571.             /**
  572.              * A PO only case is considered completed if it has POs and none of them have Draft status
  573.              */
  574.             $hasDrafts false;
  575.             $purchaseOrders $this->getPurchaseOrders();
  576.             foreach ($purchaseOrders as $purchaseOrder) {
  577.                 if ($purchaseOrder->isDraft()) {
  578.                     $hasDrafts true;
  579.                     break;
  580.                 }
  581.             }
  582.             if (count($purchaseOrders) > && !$hasDrafts) {
  583.                 return true;
  584.             }
  585.         } elseif (null !== $this->orderConfirmation) {
  586.             if ($this->orderConfirmation->isCompleted()) {
  587.                 return true;
  588.             }
  589.             if (!$this->orderConfirmation->isRejected() && !$this->orderConfirmation->isDraft()) {
  590.                 if (count($this->orderConfirmation->getSalesItems()) > && count($this->orderConfirmation->getUninvoicedSalesItems(false)) == 0) {
  591.                     return true;
  592.                 }
  593.             }
  594.         }
  595.         return false;
  596.     }
  597.     /**
  598.      * @return bool
  599.      */
  600.     public function isDraft(): bool
  601.     {
  602.         return $this->getStatus() == 'draft';
  603.     }
  604.     /**
  605.      * @ORM\PostLoad
  606.      */
  607.     public function postLoad()
  608.     {
  609.         $this->currencyOriginal $this->currency;
  610.     }
  611.     public function removeAttachment(SalesCaseAttachment $attachment): void
  612.     {
  613.         $this->attachments->removeElement($attachment);
  614.     }
  615.     /**
  616.      * @param Notification $notification
  617.      */
  618.     public function removeNotification(Notification $notification): void
  619.     {
  620.         $this->notifications->removeElement($notification);
  621.     }
  622.     /**
  623.      * @param Company $company
  624.      */
  625.     public function setCompany(Company $company): void
  626.     {
  627.         $this->company $company;
  628.         $this->currency $company->getCurrency();
  629.     }
  630.     /**
  631.      * @param string $currency
  632.      */
  633.     public function setCurrency(string $currency): void
  634.     {
  635.         $this->currency $currency;
  636.     }
  637.     /**
  638.      * @param string|null $description
  639.      */
  640.     public function setDescription(?string $description): void
  641.     {
  642.         $this->description $description;
  643.     }
  644.     /**
  645.      * @param Offer $offer
  646.      */
  647.     public function setOffer(Offer $offer): void
  648.     {
  649.         $this->offer $offer;
  650.     }
  651.     /**
  652.      * @param DateTime $opened
  653.      */
  654.     public function setOpened(DateTime $opened)
  655.     {
  656.         $this->opened $opened;
  657.     }
  658.     /**
  659.      * @param OrderConfirmation $orderConfirmation
  660.      */
  661.     public function setOrderConfirmation(OrderConfirmation $orderConfirmation)
  662.     {
  663.         $this->orderConfirmation $orderConfirmation;
  664.     }
  665.     /**
  666.      * @param string $type
  667.      */
  668.     public function setType(string $type): void
  669.     {
  670.         $this->type $type;
  671.     }
  672.     /**
  673.      * @Assert\Callback
  674.      *
  675.      * @param ExecutionContextInterface $context
  676.      */
  677.     public function validate(ExecutionContextInterface $context): void
  678.     {
  679.         if ($this->company->getCurrency() != $this->currency) {
  680.             $context
  681.                 ->buildViolation(
  682.                     'validation.currency.mismatch',
  683.                     [
  684.                         '%company_currency%' => $this->company->getCurrency(),
  685.                         '%currency%'         => $this->currency,
  686.                     ]
  687.                 )
  688.                 ->setTranslationDomain('SalesCase')
  689.                 ->atPath('company')
  690.                 ->addViolation();
  691.             $context
  692.                 ->buildViolation(
  693.                     'validation.currency.mismatch',
  694.                     [
  695.                         '%company_currency%' => $this->company->getCurrency(),
  696.                         '%currency%'         => $this->currency,
  697.                     ]
  698.                 )
  699.                 ->setTranslationDomain('SalesCase')
  700.                 ->atPath('currency')
  701.                 ->addViolation();
  702.         }
  703.         // Check if currency id being changed
  704.         // The currency can only be changed if the sales case does not yet have an Offer or and OC document
  705.         if ($this->currencyOriginal != $this->currency) {
  706.             if ($this->offer !== null || $this->orderConfirmation !== null) {
  707.                 $context
  708.                     ->buildViolation('validation.currency.readonly')
  709.                     ->setTranslationDomain('SalesCase')
  710.                     ->atPath('currency')
  711.                     ->addViolation();
  712.             }
  713.         }
  714.     }
  715. }