<?php /** * This file contains QUI\ERP\Accounting\Calc */ namespace QUI\ERP\Accounting; use DateTime; use Exception; use QUI; use QUI\ERP\Accounting\Invoice\Handler; use QUI\ERP\Accounting\Invoice\Invoice; use QUI\ERP\Currency\Currency; use QUI\ERP\Money\Price; use QUI\Interfaces\Users\User as UserInterface; use QUI\Locale; use function array_map; use function array_sum; use function class_exists; use function count; use function floatval; use function get_class; use function is_array; use function is_callable; use function is_null; use function is_string; use function json_decode; use function json_encode; use function key; use function round; use function sprintf; use function str_replace; use function strpos; use function strtotime; use function time; /** * Class Calc * Calculations for Accounting * * @info Produkt Berechnungen sind zu finden unter: QUI\ERP\Products\Utils\Calc * * @package QUI\ERP\Accounting */ class Calc { /** * Percentage calculation */ const CALCULATION_PERCENTAGE = 1; /** * Standard calculation */ const CALCULATION_COMPLEMENT = 2; /** * Set the price for the product */ const CALCULATION_COMPLETE = 3; /** * Basis calculation -> netto */ const CALCULATION_BASIS_NETTO = 1; /** * Basis calculation -> from current price */ const CALCULATION_BASIS_CURRENTPRICE = 2; /** * Basis brutto * include all price factors (from netto calculated price) * warning: it's not brutto VAT * * geht vnn der netto basis aus, welche alle price faktoren schon beinhaltet * alle felder sind in diesem price schon enthalten */ const CALCULATION_BASIS_BRUTTO = 3; /** * Berechnet auf Basis des Preises inklusive Steuern * Zum Beispiel MwSt */ const CALCULATION_BASIS_VAT_BRUTTO = 4; /** * Berechnet von Gesamtpreis */ const CALCULATION_GRAND_TOTAL = 5; /** * Special transaction attributes for currency exchange */ const TRANSACTION_ATTR_TARGET_CURRENCY = 'tx_target_currency'; const TRANSACTION_ATTR_TARGET_CURRENCY_EXCHANGE_RATE = 'tx_target_currency_exchange_rate'; const TRANSACTION_ATTR_SHOP_CURRENCY_EXCHANGE_RATE = 'tx_shop_currency_exchange_rate'; protected ?UserInterface $User = null; protected ?QUI\Locale $Locale = null; protected ?QUI\ERP\Currency\Currency $Currency = null; /** * Calc constructor. * * @param UserInterface|null $User - calculation user */ public function __construct(?UserInterface $User = null) { if (!QUI::getUsers()->isUser($User)) { $User = QUI::getUserBySession(); } $this->User = $User; $this->Locale = QUI::getLocale(); } /** * Static instance create * * @param UserInterface|null $User - optional * @return Calc */ public static function getInstance(UserInterface $User = null): Calc { if (!$User && QUI::isBackend()) { $User = QUI::getUsers()->getSystemUser(); } if (!QUI::getUsers()->isUser($User) && !QUI::getUsers()->isSystemUser($User)) { $User = QUI::getUserBySession(); } return new self($User); } /** * Set the calculation user * All calculations are made in dependence from this user * * @param UserInterface $User */ public function setUser(UserInterface $User): void { $this->User = $User; } /** * Return the calc user * * @return UserInterface|null */ public function getUser(): ?UserInterface { return $this->User; } //region locale public function getLocale(): ?Locale { return $this->Locale; } public function setLocale(QUI\Locale $Locale): void { $this->Locale = $Locale; } public function resetLocale(): void { $this->Locale = QUI::getLocale(); } //endregion /** * Return the currency * * @return Currency|null */ public function getCurrency(): ?QUI\ERP\Currency\Currency { if (is_null($this->Currency)) { $this->Currency = QUI\ERP\Currency\Handler::getDefaultCurrency(); } return $this->Currency; } /** * Calculate a complete article list * * @param ArticleList $List * @param callable|boolean $callback - optional, callback function for the data array * @return ArticleList */ public function calcArticleList(ArticleList $List, callable|bool $callback = false): ArticleList { // calc data if (!is_callable($callback)) { return $List->calc(); } // user order address $Order = $List->getOrder(); if ($Order) { $this->getUser()->setAttribute('CurrentAddress', $Order->getDeliveryAddress()); } $this->Currency = $List->getCurrency(); $articles = $List->getArticles(); $isNetto = QUI\ERP\Utils\User::isNettoUser($this->getUser()); $isEuVatUser = QUI\ERP\Tax\Utils::isUserEuVatUser($this->getUser()); $Currency = $this->getCurrency(); $precision = $Currency->getPrecision(); $subSum = 0; $nettoSum = 0; $vatArray = []; foreach ($articles as $Article) { // add netto price try { QUI::getEvents()->fireEvent( 'onQuiqqerErpCalcArticleListArticle', [$this, $Article] ); } catch (QUI\Exception $Exception) { QUI\System\Log::write($Exception->getMessage(), QUI\System\Log::LEVEL_ERROR); } $this->calcArticlePrice($Article); $articleAttributes = $Article->toArray(); $calculated = $articleAttributes['calculated']; $subSum = $subSum + $calculated['sum']; $nettoSum = $nettoSum + $calculated['nettoSum']; $articleVatArray = $calculated['vatArray']; $vat = $articleAttributes['vat']; if ($articleVatArray['text'] === '') { continue; } if (!isset($vatArray[(string)$vat])) { $vatArray[(string)$vat] = $articleVatArray; $vatArray[(string)$vat]['sum'] = 0; } $vatArray[(string)$vat]['sum'] = $vatArray[(string)$vat]['sum'] + $articleVatArray['sum']; } QUI\ERP\Debug::getInstance()->log('Berechnete Artikelliste MwSt', 'quiqqer/erp'); QUI\ERP\Debug::getInstance()->log($vatArray, 'quiqqer/erp'); try { QUI::getEvents()->fireEvent( 'onQuiqqerErpCalcArticleList', [$this, $List, $nettoSum] ); } catch (QUI\Exception $Exception) { QUI\System\Log::write($Exception->getMessage(), QUI\System\Log::LEVEL_ERROR); } /** * Calc price factors */ $priceFactors = $List->getPriceFactors(); $priceFactorSum = 0; // nur wenn wir welche benötigen, für ERP Artikel ist dies im Moment nicht wirklich nötig $nettoSubSum = $nettoSum; /* @var $PriceFactor QUI\ERP\Accounting\PriceFactors\Factor */ foreach ($priceFactors as $PriceFactor) { if ($PriceFactor->getCalculationBasis() === self::CALCULATION_GRAND_TOTAL) { $PriceFactor->setNettoSum($PriceFactor->getValue()); $PriceFactor->setValueText(''); continue; } if ($PriceFactor->getCalculation() === self::CALCULATION_COMPLEMENT) { // Standard calculation - Fester Preis $vatSum = $PriceFactor->getVatSum(); if ($isNetto) { $PriceFactor->setSum($PriceFactor->getNettoSum()); } elseif ($PriceFactor->getCalculationBasis() === self::CALCULATION_BASIS_VAT_BRUTTO) { $PriceFactor->setNettoSum($PriceFactor->getNettoSum() - $vatSum); $PriceFactor->setSum($vatSum + $PriceFactor->getNettoSum()); } else { $PriceFactor->setSum($vatSum + $PriceFactor->getNettoSum()); } // formatted $PriceFactor->setNettoSumFormatted($Currency->format($PriceFactor->getNettoSum())); $PriceFactor->setSumFormatted($Currency->format($PriceFactor->getSum())); } elseif ($PriceFactor->getCalculation() === self::CALCULATION_PERCENTAGE) { // percent - Prozent Angabe $calcBasis = $PriceFactor->getCalculationBasis(); $priceFactorValue = $PriceFactor->getValue(); $vatValue = $PriceFactor->getVat(); if ($vatValue === null) { $vatValue = QUI\ERP\Tax\Utils::getTaxByUser($this->getUser())->getValue(); } switch ($calcBasis) { default: case self::CALCULATION_BASIS_NETTO: $percentage = $priceFactorValue / 100 * $nettoSubSum; break; case self::CALCULATION_BASIS_BRUTTO: case self::CALCULATION_BASIS_CURRENTPRICE: $percentage = $priceFactorValue / 100 * $nettoSum; break; case self::CALCULATION_BASIS_VAT_BRUTTO: if ($isNetto) { $bruttoSubSum = $subSum * ($vatValue / 100 + 1); $percentage = $priceFactorValue / 100 * $bruttoSubSum; } else { $percentage = $priceFactorValue / 100 * $subSum; } break; case self::CALCULATION_GRAND_TOTAL: // starts later continue 2; } $percentage = round($percentage, $precision); $vatSum = round($PriceFactor->getVatSum(), $precision); // set netto sum $PriceFactor->setNettoSum($percentage); if ($isNetto) { $PriceFactor->setSum($PriceFactor->getNettoSum()); } elseif ($PriceFactor->getCalculationBasis() === self::CALCULATION_BASIS_VAT_BRUTTO) { $PriceFactor->setNettoSum($PriceFactor->getNettoSum() - $vatSum); $PriceFactor->setSum($vatSum + $PriceFactor->getNettoSum()); } else { $PriceFactor->setSum($vatSum + $PriceFactor->getNettoSum()); } // formatted $PriceFactor->setNettoSumFormatted($Currency->format($PriceFactor->getNettoSum())); $PriceFactor->setSumFormatted($Currency->format($PriceFactor->getSum())); } else { continue; } $nettoSum = $nettoSum + $PriceFactor->getNettoSum(); $priceFactorSum = $priceFactorSum + $PriceFactor->getNettoSum(); if ($isEuVatUser) { $PriceFactor->setEuVatStatus(true); } $vat = $PriceFactor->getVat(); $vatSum = round($PriceFactor->getVatSum(), $precision); if (!isset($vatArray[(string)$vat])) { $vatArray[(string)$vat] = [ 'vat' => $vat, 'text' => $this->getVatText($vat, $this->getUser(), $this->Locale) ]; $vatArray[(string)$vat]['sum'] = 0; } $vatArray[(string)$vat]['sum'] = $vatArray[(string)$vat]['sum'] + $vatSum; } if ($isEuVatUser) { $vatArray = []; } // vat text $vatLists = []; $vatText = []; $nettoSum = round($nettoSum, $precision); $nettoSubSum = round($nettoSubSum, $precision); $subSum = round($subSum, $precision); $bruttoSum = $nettoSum; foreach ($vatArray as $vatEntry) { $vat = $vatEntry['vat']; $vatLists[(string)$vat] = true; // liste für MWST texte $vatArray[(string)$vat]['sum'] = round($vatEntry['sum'], $precision); $bruttoSum = $bruttoSum + $vatArray[(string)$vat]['sum']; } $bruttoSum = round($bruttoSum, $precision); foreach ($vatLists as $vat => $bool) { $vatText[(string)$vat] = $this->getVatText((float)$vat, $this->getUser(), $this->Locale); } // delete 0 % vat, 0% vat is allowed to calculate more easily if (isset($vatText[0])) { unset($vatText[0]); } if (isset($vatArray[0])) { unset($vatArray[0]); } // gegenrechnung, wegen rundungsfehler if ($isNetto === false) { $priceFactorBruttoSums = 0; foreach ($priceFactors as $Factor) { if ($Factor->getCalculationBasis() !== self::CALCULATION_GRAND_TOTAL) { /* @var $Factor QUI\ERP\Products\Utils\PriceFactor */ $priceFactorBruttoSums = $priceFactorBruttoSums + round($Factor->getSum(), $precision); } } $priceFactorBruttoSum = $subSum + $priceFactorBruttoSums; $priceFactorBruttoSum = round($priceFactorBruttoSum, $precision); if ($priceFactorBruttoSum !== round($bruttoSum, $precision)) { $diff = $priceFactorBruttoSum - round($bruttoSum, $precision); // if we have a diff, we change the first vat price factor $added = false; foreach ($priceFactors as $Factor) { if ($Factor->getCalculationBasis() === self::CALCULATION_GRAND_TOTAL) { continue; } if ($Factor instanceof QUI\ERP\Products\Interfaces\PriceFactorWithVatInterface) { $Factor->setSum(round($Factor->getSum() - $diff, $precision)); $bruttoSum = round($bruttoSum, $precision); $added = true; break; } } if ($added === false) { $bruttoSum = $bruttoSum + $diff; // netto check 1cent check $bruttoVatSum = 0; foreach ($vatArray as $data) { $bruttoVatSum = $bruttoVatSum + $data['sum']; } if ($bruttoSum - $bruttoVatSum !== $nettoSum) { $nettoSum = $nettoSum + $diff; } } } // counterbalance - gegenrechnung // works only for one vat entry if (count($vatArray) === 1 && $isNetto) { $vat = key($vatArray); $netto = $bruttoSum / ((float)$vat / 100 + 1); $vatSum = $bruttoSum - $netto; $vatSum = round($vatSum, $Currency->getPrecision()); $diff = abs($vatArray[(string)$vat]['sum'] - $vatSum); if ($diff <= 0.019) { $vatArray[(string)$vat]['sum'] = $vatSum; } } } if (empty($bruttoSum) || empty($nettoSum)) { $bruttoSum = 0; $nettoSum = 0; foreach ($vatArray as $vat => $entry) { $vatArray[(string)$vat]['sum'] = 0; } } // look if CALCULATION_GRAND_TOTAL $grandSubSum = $bruttoSum; foreach ($priceFactors as $Factor) { if ($Factor->getCalculationBasis() === self::CALCULATION_GRAND_TOTAL) { $value = $Factor->getValue(); $bruttoSum = $bruttoSum + $value; if ($bruttoSum < 0) { $bruttoSum = 0; } } } $callback([ 'sum' => $bruttoSum, 'subSum' => $subSum, 'grandSubSum' => $grandSubSum, 'nettoSum' => $nettoSum, 'nettoSubSum' => $nettoSubSum, 'vatArray' => $vatArray, 'vatText' => $vatText, 'isEuVat' => $isEuVatUser, 'isNetto' => $isNetto, 'currencyData' => $this->getCurrency()->toArray() ]); return $List; } /** * Calculate the price of an article * * @param Article $Article * @param bool|callable $callback * @return mixed */ public function calcArticlePrice(Article $Article, $callback = false) { // calc data if (!is_callable($callback)) { $Article->calc($this); return $Article->getPrice(); } $isNetto = QUI\ERP\Utils\User::isNettoUser($this->getUser()); $isEuVatUser = QUI\ERP\Tax\Utils::isUserEuVatUser($this->getUser()); $Currency = $Article->getCurrency(); if (!$Currency) { $Currency = $this->getCurrency(); } $nettoPrice = $Article->getUnitPriceUnRounded()->value(); $nettoPrice = round($nettoPrice, $Currency->getPrecision()); $nettoPriceNotRounded = $Article->getUnitPriceUnRounded()->getValue(); $vat = $Article->getVat(); $quantity = $Article->getQuantity(); $basisNettoPrice = $nettoPrice; $nettoSubSum = $this->round($nettoPrice * $Article->getQuantity()); if ($isEuVatUser) { $vat = 0; } // discounts $Discount = $Article->getDiscount(); if ($Discount) { switch ($Discount->getCalculation()) { // einfache Zahl, Währung --- kein Prozent case Calc::CALCULATION_COMPLEMENT: $nettoPrice = $nettoPrice - ($Discount->getValue() / $Article->getQuantity()); $nettoPriceNotRounded = $nettoPriceNotRounded - ($Discount->getValue() / $Article->getQuantity()); break; // Prozent Angabe case Calc::CALCULATION_PERCENTAGE: $percentage = $Discount->getValue() / 100 * $nettoPrice; $nettoPrice = $nettoPrice - $percentage; $nettoPriceNotRounded = $nettoPriceNotRounded - $percentage; break; } } $vatSum = $nettoPrice * ($vat / 100); $precision = $Currency->getPrecision(); $vatSum = round($vatSum, $precision); $priceSum = $nettoPrice + $vatSum; $bruttoPrice = round($priceSum, $precision); if (!$isNetto) { // korrektur rechnung / 1 cent problem $checkBrutto = $nettoPriceNotRounded * ($vat / 100 + 1); $checkBrutto = round($checkBrutto, $Currency->getPrecision()); $checkVat = $checkBrutto - $nettoPriceNotRounded; if ($nettoPrice + $checkVat !== $checkBrutto) { $diff = round( $nettoPrice + $checkVat - $checkBrutto, $Currency->getPrecision() ); $checkVat = $checkVat - $diff; } // sum $checkVat = round($checkVat * $Article->getQuantity(), $Currency->getPrecision()); $nettoSum = $this->round($nettoPrice * $Article->getQuantity()); $vatSum = $nettoSum * ($vat / 100); // korrektur rechnung / 1 cent problem if ($checkBrutto !== $bruttoPrice) { $bruttoPrice = $checkBrutto; $vatSum = $checkVat; } // Related: pcsg/buero#344 // Related: pcsg/buero#436 if ($nettoSum + $checkVat !== $bruttoPrice * $quantity) { $diff = $nettoSum + $checkVat - ($bruttoPrice * $quantity); $vatSum = $vatSum - $diff; $vatSum = round($vatSum, $precision); } // if the user is brutto // and we have a quantity // we need to calc first the brutto product price of one product // -> because of 1 cent rounding error $bruttoSum = $bruttoPrice * $Article->getQuantity(); } else { // sum $nettoSum = $this->round($nettoPrice * $Article->getQuantity()); $vatSum = $nettoSum * ($vat / 100); $bruttoSum = $this->round($nettoSum + $vatSum); } $price = $isNetto ? $nettoPrice : $bruttoPrice; $sum = $isNetto ? $nettoSum : $bruttoSum; $basisPrice = $isNetto ? $basisNettoPrice : $basisNettoPrice + ($basisNettoPrice * $vat / 100); $basisPrice = round($basisPrice, QUI\ERP\Defaults::getPrecision()); $vatArray = [ 'vat' => $vat, 'sum' => $vatSum, 'text' => $this->getVatText($vat, $this->getUser(), $this->Locale) ]; QUI\ERP\Debug::getInstance()->log( 'Kalkulierter Artikel Preis ' . $Article->getId(), 'quiqqer/erp' ); $data = [ 'basisPrice' => $basisPrice, 'price' => $price, 'sum' => $sum, 'nettoBasisPrice' => $basisNettoPrice, 'nettoPrice' => $nettoPrice, 'nettoSubSum' => $nettoSubSum, 'nettoSum' => $nettoSum, 'currencyData' => $this->getCurrency()->toArray(), 'vatArray' => $vatArray, 'vatText' => $vatArray['text'], 'isEuVat' => $isEuVatUser, 'isNetto' => $isNetto ]; QUI\ERP\Debug::getInstance()->log($data, 'quiqqer/erp'); $callback($data); return $Article->getPrice(); } /** * Rounds the value via shop config * * @param string|int|float $value * @return float */ public function round($value): float { $decimalSeparator = $this->getUser()->getLocale()->getDecimalSeparator(); $groupingSeparator = $this->getUser()->getLocale()->getGroupingSeparator(); $precision = QUI\ERP\Defaults::getPrecision(); if (strpos($value, $decimalSeparator) && $decimalSeparator != '.') { $value = str_replace($groupingSeparator, '', $value); } $value = str_replace(',', '.', $value); $value = floatval($value); $value = round($value, $precision); return $value; } /** * Return the tax message for an user * * @return string */ public function getVatTextByUser(): string { try { $Tax = QUI\ERP\Tax\Utils::getTaxByUser($this->getUser()); } catch (QUI\Exception) { return ''; } return $this->getVatText($Tax->getValue(), $this->getUser(), $this->Locale); } /** * Return tax text * eq: incl or zzgl * * @param float|int $vat * @param UserInterface $User * @param null|Locale $Locale - optional * * @return string */ public static function getVatText( float|int $vat, UserInterface $User, QUI\Locale $Locale = null ): string { if ($Locale === null) { $Locale = QUI::getLocale(); } if (QUI\ERP\Utils\User::isNettoUser($User)) { if (QUI\ERP\Tax\Utils::isUserEuVatUser($User)) { return $Locale->get( 'quiqqer/tax', 'message.vat.text.netto.EUVAT', ['vat' => $vat] ); } // vat ist leer und kein EU vat user if (!$vat) { return ''; } return $Locale->get( 'quiqqer/tax', 'message.vat.text.netto', ['vat' => $vat] ); } if (QUI\ERP\Tax\Utils::isUserEuVatUser($User)) { return $Locale->get( 'quiqqer/tax', 'message.vat.text.brutto.EUVAT', ['vat' => $vat] ); } // vat ist leer und kein EU vat user if (!$vat) { return ''; } return $Locale->get( 'quiqqer/tax', 'message.vat.text.brutto', ['vat' => $vat] ); } /** * Calculates the individual amounts paid of an invoice * * @param Invoice $Invoice * @return array * * @throws QUI\ERP\Exception * * @deprecated use calculatePayments */ public static function calculateInvoicePayments(Invoice $Invoice): array { return self::calculatePayments($Invoice); } /** * Calculates the individual amounts paid of an invoice / order * * @param mixed $ToCalculate * @return array * * @throws QUI\ERP\Exception|QUI\Exception */ public static function calculatePayments($ToCalculate): array { if (self::isAllowedForCalculation($ToCalculate) === false) { QUI\ERP\Debug::getInstance()->log( 'Calc->calculatePayments(); Object is not allowed to calculate ' . get_class($ToCalculate) ); throw new QUI\ERP\Exception('Object is not allowed to calculate ' . get_class($ToCalculate)); } QUI\ERP\Debug::getInstance()->log( 'Calc->calculatePayments(); Transaction' ); // if payment status is paid, take it immediately and do not query any transactions if ($ToCalculate->getAttribute('paid_status') === QUI\ERP\Constants::PAYMENT_STATUS_PAID) { $paidData = $ToCalculate->getAttribute('paid_data'); $paid = 0; if (!is_array($paidData)) { $paidData = json_decode($paidData, true); } if (!is_array($paidData)) { $paidData = []; } foreach ($paidData as $entry) { if (isset($entry['amount'])) { $paid = $paid + floatval($entry['amount']); } } $ToCalculate->setAttribute('paid', $paid); $ToCalculate->setAttribute('toPay', 0); QUI\ERP\Debug::getInstance()->log([ 'paidData' => $ToCalculate->getAttribute('paid_data'), 'paidDate' => $ToCalculate->getAttribute('paid_date'), 'paidStatus' => $ToCalculate->getAttribute('paid_status'), 'paid' => $ToCalculate->getAttribute('paid'), 'toPay' => $ToCalculate->getAttribute('toPay') ]); return [ 'paidData' => $ToCalculate->getAttribute('paid_data'), 'paidDate' => $ToCalculate->getAttribute('paid_date'), 'paidStatus' => $ToCalculate->getAttribute('paid_status'), 'paid' => $ToCalculate->getAttribute('paid'), 'toPay' => $ToCalculate->getAttribute('toPay') ]; } // calc with transactions $Transactions = QUI\ERP\Accounting\Payments\Transactions\Handler::getInstance(); $transactions = $Transactions->getTransactionsByHash($ToCalculate->getHash()); $calculations = $ToCalculate->getArticles()->getCalculations(); if (!isset($calculations['sum'])) { $calculations['sum'] = 0; } $paidData = []; $paidDate = 0; $sum = 0; $total = $calculations['sum']; QUI\ERP\Debug::getInstance()->log( 'Calc->calculatePayments(); total: ' . $total ); $isValidTimeStamp = function ($timestamp) { try { new DateTime('@' . $timestamp); } catch (Exception $e) { return false; } return true; }; $CalculateCurrency = $ToCalculate->getCurrency(); $ShopCurrency = QUI\ERP\Defaults::getCurrency(); foreach ($transactions as $Transaction) { if (!$Transaction->isComplete()) { // don't add incomplete transactions continue; } // calculate the paid amount $amount = Price::validatePrice($Transaction->getAmount()); $TransactionCurrency = $Transaction->getCurrency(); // If necessary, convert from transaction currency to calculation object currency if ($CalculateCurrency->getCode() !== $TransactionCurrency->getCode()) { $targetCurrencyCode = $Transaction->getData( self::TRANSACTION_ATTR_TARGET_CURRENCY ); $targetCurrencyExchangeRate = $Transaction->getData( self::TRANSACTION_ATTR_TARGET_CURRENCY_EXCHANGE_RATE ); $shopCurrencyExchangeRate = $Transaction->getData( self::TRANSACTION_ATTR_SHOP_CURRENCY_EXCHANGE_RATE ); /* * $amount has to DIVIDED by the exchange rate because the exchange rate is always * in relation to the base (shop) currency to the given currency. * * Example: From ETH to EUR -> The exchange rate here is the rate that turn EUR into ETH; so to * get ETH to EUR you have to divide the ETH value by the exchange rate. */ if ($targetCurrencyCode === $CalculateCurrency->getCode() && $targetCurrencyExchangeRate) { $amount /= $targetCurrencyExchangeRate; } elseif ($ShopCurrency === $CalculateCurrency->getCode() && $shopCurrencyExchangeRate) { $amount /= $shopCurrencyExchangeRate; } else { $amount = $TransactionCurrency->convert($amount, $CalculateCurrency); QUI\System\Log::addWarning( sprintf( 'The currency of transaction "%s" for calculation of object %s (%s) is "%s" and differs' . ' from the currency of the calculation object ("%s"). But the transaction does not' . ' contain an exchange rate from "%s" to "%s". Thus, the exchange rate that is currently' . ' live in the system is used for converting from "%s" to "%s".', $Transaction->getTxId(), $ToCalculate->getId(), get_class($ToCalculate), $TransactionCurrency->getCode(), $CalculateCurrency->getCode(), $TransactionCurrency->getCode(), $CalculateCurrency->getCode(), $TransactionCurrency->getCode(), $CalculateCurrency->getCode() ) ); } $amount = $CalculateCurrency->amount($amount); } // set the newest date $date = $Transaction->getDate(); if ($isValidTimeStamp($date) === false) { $date = strtotime($date); if ($isValidTimeStamp($date) === false) { $date = time(); } } else { $date = (int)$date; } if ($date > $paidDate) { $paidDate = $date; } // Falls das gezahlte mehr ist if ($total < ($sum + $amount)) { $amount = $total - $sum; // @todo Information in Rechnung hinterlegen // @todo Automatische Gutschrift erstellen } $sum = $sum + $amount; $paidData[] = [ 'amount' => $amount, 'date' => $date, 'txid' => $Transaction->getTxId() ]; } $paid = Price::validatePrice($sum); $toPay = Price::validatePrice($calculations['sum']); // workaround fix if ($ToCalculate->getAttribute('paid_date') != $paidDate) { try { QUI::getDataBase()->update( Handler::getInstance()->invoiceTable(), ['paid_date' => $paidDate], ['id' => $ToCalculate->getCleanId()] ); } catch (QUI\Database\Exception $Exception) { QUI\System\Log::writeException($Exception); throw new QUI\ERP\Exception( ['quiqqer/erp', 'exception.something.went.wrong'], $Exception->getCode() ); } } $ToCalculate->setAttribute('paid_data', json_encode($paidData)); $ToCalculate->setAttribute('paid_date', $paidDate); $ToCalculate->setAttribute('paid', $sum); $ToCalculate->setAttribute('toPay', $toPay - $paid); if ( $ToCalculate instanceof QUI\ERP\Order\AbstractOrder && $ToCalculate->getAttribute('paid_status') === QUI\ERP\Constants::PAYMENT_STATUS_PLAN ) { // Leave everything as it is because a subscription plan order can never be set to "paid" } elseif ( $ToCalculate->getAttribute('paid_status') === QUI\ERP\Constants::TYPE_INVOICE_REVERSAL || $ToCalculate->getAttribute('paid_status') === QUI\ERP\Constants::TYPE_INVOICE_CANCEL || $ToCalculate->getAttribute('paid_status') === QUI\ERP\Constants::PAYMENT_STATUS_DEBIT ) { // Leave everything as it is } elseif ((float)$ToCalculate->getAttribute('toPay') == 0) { $ToCalculate->setAttribute('paid_status', QUI\ERP\Constants::PAYMENT_STATUS_PAID); } elseif ($ToCalculate->getAttribute('paid') == 0) { $ToCalculate->setAttribute('paid_status', QUI\ERP\Constants::PAYMENT_STATUS_OPEN); } elseif ( $ToCalculate->getAttribute('toPay') && $calculations['sum'] != $ToCalculate->getAttribute('paid') ) { $ToCalculate->setAttribute('paid_status', QUI\ERP\Constants::PAYMENT_STATUS_PART); } QUI\ERP\Debug::getInstance()->log([ 'paidData' => $paidData, 'paidDate' => $ToCalculate->getAttribute('paid_date'), 'paid' => $ToCalculate->getAttribute('paid'), 'toPay' => $ToCalculate->getAttribute('toPay'), 'paidStatus' => $ToCalculate->getAttribute('paid_status'), 'sum' => $sum ]); return [ 'paidData' => $paidData, 'paidDate' => $ToCalculate->getAttribute('paid_date'), 'paidStatus' => $ToCalculate->getAttribute('paid_status'), 'paid' => $ToCalculate->getAttribute('paid'), 'toPay' => $ToCalculate->getAttribute('toPay') ]; } /** * Is the object allowed for calculation * * @param mixed $ToCalculate * @return bool */ public static function isAllowedForCalculation(mixed $ToCalculate): bool { if ($ToCalculate instanceof QUI\ERP\ErpEntityInterface) { return true; } return false; } /** * Calculate the total of the invoice list * * @param array $invoiceList - list of invoice array * @param QUI\ERP\Currency\Currency|null $Currency * @return array */ public static function calculateTotal(array $invoiceList, QUI\ERP\Currency\Currency $Currency = null): array { if ($Currency === null) { try { $currency = json_decode($invoiceList[0]['currency_data'], true); $Currency = QUI\ERP\Currency\Handler::getCurrency($currency['code']); } catch (QUI\Exception $Exception) { $Currency = QUI\ERP\Defaults::getCurrency(); } } if (!count($invoiceList)) { $display = $Currency->format(0); return [ 'netto_toPay' => 0, 'netto_paid' => 0, 'netto_total' => 0, 'display_netto_toPay' => $display, 'display_netto_paid' => $display, 'display_netto_total' => $display, 'vat_toPay' => 0, 'vat_paid' => 0, 'vat_total' => 0, 'display_vat_toPay' => $display, 'display_vat_paid' => $display, 'display_vat_total' => $display, 'brutto_toPay' => 0, 'brutto_paid' => 0, 'brutto_total' => 0, 'display_brutto_toPay' => $display, 'display_brutto_paid' => $display, 'display_brutto_total' => $display ]; } $nettoTotal = 0; $vatTotal = 0; $bruttoToPay = 0; $bruttoPaid = 0; $bruttoTotal = 0; $vatPaid = 0; $nettoToPay = 0; foreach ($invoiceList as $invoice) { // if (isset($invoice['type']) && (int)$invoice['type'] === Handler::TYPE_INVOICE_CANCEL || // isset($invoice['type']) && (int)$invoice['type'] === Handler::TYPE_INVOICE_STORNO // ) { // continue; // } // soll doch mit berechnet werden $invBruttoSum = floatval($invoice['calculated_sum']); $invVatSum = floatval($invoice['calculated_vatsum']); $invPaid = floatval($invoice['calculated_paid']); $invToPay = floatval($invoice['calculated_toPay']); $invNettoTotal = floatval($invoice['calculated_nettosum']); $invVatSumPC = QUI\Utils\Math::percent($invVatSum, $invBruttoSum); $invBruttoSum = round($invBruttoSum, $Currency->getPrecision()); $invVatSum = round($invVatSum, $Currency->getPrecision()); $invPaid = round($invPaid, $Currency->getPrecision()); $invToPay = round($invToPay, $Currency->getPrecision()); $invNettoTotal = round($invNettoTotal, $Currency->getPrecision()); if ($invoice['paid_status'] === QUI\ERP\Constants::PAYMENT_STATUS_PAID) { $invPaid = $invBruttoSum; } if ($invVatSumPC) { if ($invToPay === 0.0) { $invVatPaid = $invVatSum; } else { $invVatPaid = round($invPaid * $invVatSumPC / 100, $Currency->getPrecision()); } } else { $invVatPaid = 0; } $invNettoPaid = $invPaid - $invVatPaid; $invNettoToPay = $invNettoTotal - $invNettoPaid; if ($invToPay === 0.0) { $invNettoToPay = 0; } // complete + addition $vatPaid = $vatPaid + $invVatPaid; $bruttoTotal = $bruttoTotal + $invBruttoSum; $bruttoPaid = $bruttoPaid + $invPaid; //$bruttoToPay = $bruttoToPay + $invToPay; $nettoToPay = $nettoToPay + $invNettoToPay; $vatTotal = $vatTotal + $invVatSum; $nettoTotal = $nettoTotal + $invNettoTotal; } // netto calculation $nettoPaid = $bruttoPaid - $vatPaid; // vat calculation $vatToPay = $vatTotal - $vatPaid; $bruttoToPay = $bruttoTotal - $bruttoPaid; return [ 'netto_toPay' => $nettoToPay, 'netto_paid' => $nettoPaid, 'netto_total' => $nettoTotal, 'display_netto_toPay' => $Currency->format($nettoToPay), 'display_netto_paid' => $Currency->format($nettoPaid), 'display_netto_total' => $Currency->format($nettoTotal), 'vat_toPay' => $nettoPaid, 'vat_paid' => $vatPaid, 'vat_total' => $vatTotal, 'display_vat_toPay' => $Currency->format($vatToPay), 'display_vat_paid' => $Currency->format($vatPaid), 'display_vat_total' => $Currency->format($vatTotal), 'brutto_toPay' => $bruttoToPay, 'brutto_paid' => $bruttoPaid, 'brutto_total' => $bruttoTotal, 'display_brutto_toPay' => $Currency->format($bruttoToPay), 'display_brutto_paid' => $Currency->format($bruttoPaid), 'display_brutto_total' => $Currency->format($bruttoTotal) ]; } /** * Return the total of all vats * * @param string|array $vatArray * @return float|int */ public static function calculateTotalVatOfInvoice($vatArray) { if (is_string($vatArray)) { $vatArray = json_decode($vatArray, true); } if (!is_array($vatArray)) { return 0; } return array_sum( array_map(function ($vat) { return $vat['sum']; }, $vatArray) ); } }