Newer
Older
/**
* This file contains QUI\ERP\Accounting\Invoice\Invoice
*/
use QUI\ERP\Accounting\ArticleListUnique;
use QUI\ERP\Accounting\Calculations;
use QUI\ERP\Accounting\Invoice\Utils\Invoice as InvoiceUtils;
use QUI\ERP\Accounting\Payments\Transactions\Transaction;
use QUI\ERP\Address as ErpAddress;
use QUI\ERP\Exception;
use QUI\ERP\Output\Output as ERPOutput;
use QUI\ERP\Shipping\Api\ShippingInterface;
use QUI\ExceptionStack;
use QUI\ERP\ErpEntityInterface;
use QUI\ERP\ErpTransactionsInterface;
use QUI\ERP\ErpCopyInterface;
use QUI\Permissions\Permission;
use function array_key_exists;
use function class_exists;
use function date;
use function is_array;
use function is_numeric;
use function is_string;
use function json_decode;
use function json_encode;
* - This class present a posted invoice
class Invoice extends QUI\QDOM implements ErpEntityInterface, ErpTransactionsInterface, ErpCopyInterface
const DUNNING_LEVEL_OPEN = 0; // No Dunning -> Keine Mahnung
const DUNNING_LEVEL_REMIND = 1; // Payment reminding -> Zahlungserinnerung
const DUNNING_LEVEL_DUNNING = 2; // Dunning -> Erste Mahnung
const DUNNING_LEVEL_DUNNING2 = 3; // Second dunning -> Zweite Mahnung
const DUNNING_LEVEL_COLLECTION = 4; // Collection -> Inkasso
/**
* Invoice type
*
* @var int
*/
/**
* @var string
*/
protected string $id_with_prefix = '';

Henning Leutz
committed
/**
* @var string
*/

Henning Leutz
committed
protected array $data = [];
/**
* variable data for developers
*
* @var array
*/
protected array $customData = [];
protected array $paymentData = [];
* @var null|ShippingInterface
protected ShippingInterface|null $Shipping = null;
/**
* Invoice constructor.
*
* @param $id
* @param Handler $Handler
*/
public function __construct($id, Handler $Handler)
{
$invoiceData = $Handler->getInvoiceData($id);
$this->setAttributes($invoiceData);
$this->prefix = $this->getAttribute('id_prefix');
if ($this->prefix === false) {
$this->prefix = Settings::getInstance()->getInvoicePrefix();
}
$this->type = QUI\ERP\Constants::TYPE_INVOICE;
if (!empty($invoiceData['id_with_prefix'])) {
$this->id_with_prefix = $invoiceData['id_with_prefix'];
} else {
$this->id_with_prefix = $this->prefix . $this->id;
}
switch ((int)$this->getAttribute('type')) {
case QUI\ERP\Constants::TYPE_INVOICE:
case QUI\ERP\Constants::TYPE_INVOICE_TEMPORARY:
case QUI\ERP\Constants::TYPE_INVOICE_CREDIT_NOTE:
case QUI\ERP\Constants::TYPE_INVOICE_REVERSAL:
case QUI\ERP\Constants::TYPE_INVOICE_CANCEL:
$this->type = (int)$this->getAttribute('type');
break;
}
if ($this->getAttribute('data')) {
$this->data = json_decode($this->getAttribute('data'), true) ?? [];
if ($this->getAttribute('custom_data')) {
$this->customData = json_decode($this->getAttribute('custom_data'), true) ?? [];
// fallback customer files
if (!empty($this->data['customer_files']) && empty($this->customData['customer_files'])) {
$this->customData['customer_files'] = $this->data['customer_files'];
}

Henning Leutz
committed
if ($this->getAttribute('global_process_id')) {
$this->globalProcessId = $this->getAttribute('global_process_id');
}

Henning Leutz
committed
if ($this->getAttribute('payment_data')) {
$paymentData = QUI\Security\Encryption::decrypt($this->getAttribute('payment_data'));
$this->paymentData = $paymentData ?? [];
// shipping
if ($this->getAttribute('shipping_id')) {
$shippingData = $this->getAttribute('shipping_data');
if (class_exists('QUI\ERP\Shipping\Types\ShippingUnique')) {
$this->Shipping = new QUI\ERP\Shipping\Types\ShippingUnique($shippingData);
}
}
// consider contact person in address
if (
!empty($this->getAttribute('invoice_address')) &&
!empty($this->getAttribute('contact_person'))
) {
$invoiceAddress = $this->getAttribute('invoice_address');
$invoiceAddress['contactPerson'] = $this->getAttribute('contact_person');
$this->setAttribute('invoice_address', json_encode($invoiceAddress));
}
/**
* Return the invoice id
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* Rechnungsnummer
*
* @return string
*/
public function getPrefixedNumber(): string
{
return $this->id_with_prefix;
}
/**
* alias for getUUID()
* (Vorgangsnummer)
*
* @return string
{
return $this->getUUID();
}
/**
* Return the uuid of the invoice
* (Vorgangsnummer)
*
* @return string
*/
public function getUUID(): string
{
return $this->getAttribute('hash');
}

Henning Leutz
committed
/**
* Return the global process id
*
* @return string
*/

Henning Leutz
committed
{
return $this->globalProcessId;
}
/**
* Returns only the integer id
*
* @return int
/**
* Return the invoice view
*
* @return InvoiceView
* @throws \QUI\ERP\Accounting\Invoice\Exception
{
return new InvoiceView($this);
}
/**
* Return the unique article list
*
* @return ArticleListUnique
* @throws QUI\ERP\Exception|QUI\Exception
public function getArticles(): ArticleListUnique
$articles = $this->getAttribute('articles');
if (is_string($articles)) {
$articles = json_decode($articles, true);

Henning Leutz
committed
$List = new ArticleListUnique($articles, $this->getCustomer());
$List->setLocale($this->getCustomer()->getLocale());
// accounting currency, if exists
$accountingCurrencyData = $this->getCustomDataEntry('accountingCurrencyData');
try {
if ($accountingCurrencyData) {
$List->setExchangeCurrency(
new QUI\ERP\Currency\Currency(
$accountingCurrencyData['accountingCurrency']
)
);
$List->setExchangeRate($accountingCurrencyData['rate']);
}
} catch (QUI\Exception) {
/**
* Return the invoice currency
*
* @return QUI\ERP\Currency\Currency
* @throws QUI\Exception
public function getCurrency(): QUI\ERP\Currency\Currency
{
$currency = $this->getAttribute('currency_data');
if (!$currency) {
if (is_string($currency)) {
$currency = json_decode($currency, true);
}
if (!$currency || !isset($currency['code'])) {
$Currency = QUI\ERP\Currency\Handler::getCurrency($currency['code']);
if (isset($currency['rate'])) {
$Currency->setExchangeRate($currency['rate']);
}
return $Currency;

Henning Leutz
committed
/**
* @return QUI\ERP\User
* @throws QUI\ERP\Exception|QUI\Exception

Henning Leutz
committed
*/

Henning Leutz
committed
{
$invoiceAddress = $this->getAttribute('invoice_address');
$customerData = $this->getAttribute('customer_data');

Henning Leutz
committed
if (is_string($customerData)) {
$customerData = json_decode($customerData, true);

Henning Leutz
committed
}
// address
if (is_string($invoiceAddress)) {
$invoiceAddress = json_decode($invoiceAddress, true);

Henning Leutz
committed
}
$userData = $customerData;
if (!isset($userData['isCompany'])) {
$userData['isCompany'] = false;
if (isset($invoiceAddress['company'])) {
$userData['isCompany'] = true;
}
}
$userData['address'] = $invoiceAddress;
if (!isset($userData['id'])) {
$userData['id'] = $this->getAttribute('customer_id');
}
// Country fallback
if (empty($userData['country'])) {
if (!empty($invoiceAddress['country'])) {
$userData['country'] = $invoiceAddress['country'];
} else {
$userData['country'] = QUI\ERP\Defaults::getCountry()->getCode();
}
}

Henning Leutz
committed
return new QUI\ERP\User($userData);
}
/**
* @return QUI\ERP\User

Henning Leutz
committed
*/

Henning Leutz
committed
{

Henning Leutz
committed
'firstname' => '',
'lastname' => $this->getAttribute('editor_name'),
'lang' => '',
];
if (is_numeric($this->getAttribute('editor_id'))) {
$params['id'] = $this->getAttribute('editor_id');
}
if (is_string($this->getAttribute('editor_id'))) {
$params['uuid'] = $this->getAttribute('editor_id');
}
return new QUI\ERP\User($params);

Henning Leutz
committed
}
/**
* Return the payment paid status information
* - How many has already been paid
* - How many must be paid
*
* @return array
public function getPaidStatusInformation(): array

Henning Leutz
committed
$oldStatus = $this->getAttribute('paid_status');
QUI\ERP\Accounting\Calc::calculatePayments($this);
// the status is another as canceled, to we must update the invoice data

Henning Leutz
committed
if ($this->getAttribute('paid_status') !== $oldStatus) {
$this->calculatePayments();
}
if ($this->getInvoiceType() === QUI\ERP\Constants::TYPE_INVOICE_STORNO) {
//$this->setAttribute('paid_status', QUI\ERP\Constants::PAYMENT_STATUS_CANCELED);
'paidData' => $this->getAttribute('paid_data'),
'paidDate' => $this->getAttribute('paid_date'),
'paid' => $this->getAttribute('paid'),
'toPay' => $this->getAttribute('toPay')
/**
* Return an entry in the payment data (decrypted)
*
* @param string $key
* @return mixed|null
*/
public function getPaymentDataEntry(string $key): mixed
return $this->paymentData[$key];
}
return null;
}
/**
* ALIAS for $this->getPaymentDataEntry to be consistent with InvoiceTemporary.
*
* @param string $key
* @return mixed|null
*/
public function getPaymentData(string $key): mixed
return $this->getPaymentDataEntry($key);
}
/**
* can a refund be made?
*
* @return bool
*/
{
$transactions = InvoiceUtils::getTransactionsByInvoice($this);
/* @var $Transaction Transaction */
foreach ($transactions as $Transaction) {
/* @var $Payment QUI\ERP\Accounting\Payments\Api\AbstractPayment */
$Payment = $Transaction->getPayment();
return true;
}
}
return false;
}
if ($this->getAttribute('toPay') === false) {
try {
QUI\ERP\Accounting\Calc::calculatePayments($this);
} catch (QUI\Exception $Exception) {
QUI\System\Log::writeDebugException($Exception);
return $this->getAttribute('toPay') <= 0;
}
/**
* Return the presentational payment method
*
* @return Payment
*/
{
$data = $this->getAttribute('payment_method_data');
if (!$data) {
QUI\System\Log::addCritical(
'Error with invoice ' . $this->getUUID() . '. No payment Data available'
);
return new Payment([]);
}
if (is_string($data)) {
$data = json_decode($data, true);
}
return new Payment($data);
}
/**
* Return the Shipping, if a shipping is set
*
* @return int|QUI\ERP\Shipping\Types\ShippingUnique|null
*/
public function getShipping(): int|QUI\ERP\Shipping\Types\ShippingUnique|null
{
return $this->Shipping;
}
* Return the invoice type
*
* - Handler::TYPE_INVOICE
* - Handler::TYPE_INVOICE_TEMPORARY
* - Handler::TYPE_INVOICE_CREDIT_NOTE
* - Handler::TYPE_INVOICE_CANCEL
*
* @return int
}
/**
* Cancel the invoice
* (Reversal, Storno, Cancel)
*
* @param null|QUI\Interfaces\Users\User $PermissionUser
* @return ?QUI\ERP\ErpEntityInterface
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function reversal(
string $reason = '',
QUI\Interfaces\Users\User $PermissionUser = null
): ?QUI\ERP\ErpEntityInterface {
if (!Settings::getInstance()->get('invoice', 'storno')) {
// @todo implement credit note
throw new Exception([
'quiqqer/invoice',
'exception.canceled.invoice.is.not.allowed'
]);
}
if ($PermissionUser === null) {
$PermissionUser = QUI::getUserBySession();
}
'quiqqer.invoice.reversal',
$PermissionUser
);
'quiqqer/invoice',
'exception.missing.reason.in.reversal'
// if invoice is parted paid, it could not be canceled
$this->getPaidStatusInformation();
if ($this->getAttribute('paid_status') == QUI\ERP\Constants::PAYMENT_STATUS_PART) {
'quiqqer/invoice',
'exception.parted.invoice.cant.be.canceled'
if ($this->getAttribute('paid_status') == QUI\ERP\Constants::PAYMENT_STATUS_CANCELED) {
'quiqqer/invoice',
'exception.canceled.invoice.cant.be.canceled'

Henning Leutz
committed
QUI::getEvents()->fireEvent('quiqqerInvoiceReversal', [$this]);
$this->addHistory(
QUI::getLocale()->get(
'quiqqer/invoice',

Henning Leutz
committed
QUI::getUsers()->getSystemUser(),
$this->getGlobalProcessId()
);
//$CreditNote->setInvoiceType(Handler::TYPE_INVOICE_REVERSAL);
// Cancellation invoice extra text
$localeCode = QUI::getLocale()->getLocalesByLang(
QUI::getLocale()->getCurrent()
);
);
$currentDate = $this->getAttribute('date');
if (!$currentDate) {
}
$message = $this->getCustomer()->getLocale()->get(
'quiqqer/invoice',
'message.invoice.cancellationInvoice.additionalInvoiceText',
[
'id' => $this->getUUID(),
'date' => $Formatter->format($currentDate)
]
);
if (empty($message)) {
$message = '';
}
// saving copy
$Reversal->setAttribute('additional_invoice_text', $message);
$Reversal->save(QUI::getUsers()->getSystemUser());
try {
Permission::checkPermission('quiqqer.invoice.canEditCancelInvoice', $PermissionUser);
} catch (QUI\Exception) {
$Reversal->post(QUI::getUsers()->getSystemUser());
$this->addHistory(
QUI::getLocale()->get(
'quiqqer/invoice',
'history.message.reversal.created',
'creditNoteId' => $Reversal->getUUID()
$this->type = QUI\ERP\Constants::TYPE_INVOICE_CANCEL;
$Reversal = $Reversal->post(QUI::getUsers()->getSystemUser());
$this->data['canceledId'] = $Reversal->getUUID();
QUI::getDataBase()->update(
Handler::getInstance()->invoiceTable(),
'type' => $this->type,
'data' => json_encode($this->data),
'paid_status' => QUI\ERP\Constants::PAYMENT_STATUS_CANCELED
$this->addComment($reason, QUI::getUsers()->getSystemUser());
QUI::getEvents()->fireEvent(
*
* @param string $reason
* @param null|QUI\Interfaces\Users\User $PermissionUser
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function cancellation(string $reason, QUI\Interfaces\Users\User $PermissionUser = null): int|string
return $this->reversal($reason, $PermissionUser)->getUUID();
}
/**
* Alias for reversal
* @param null|User $PermissionUser
* @return int|string - ID of the new
*
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function storno(string $reason, QUI\Interfaces\Users\User $PermissionUser = null): int|string
return $this->reversal($reason, $PermissionUser)->getUUID();
}
/**
* Copy the invoice to a temporary invoice
*
* @param null|QUI\Interfaces\Users\User $PermissionUser
* @param bool|string $globalProcessId
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function copy(
QUI\Interfaces\Users\User $PermissionUser = null,
bool|string $globalProcessId = false
): InvoiceTemporary {
if ($PermissionUser === null) {
$PermissionUser = QUI::getUserBySession();
}
'quiqqer.invoice.copy',
$PermissionUser
);
$User = QUI::getUserBySession();
$this->addHistory(
QUI::getLocale()->get(
'quiqqer/invoice',
'history.message.copy',

Henning Leutz
committed
QUI::getEvents()->fireEvent('quiqqerInvoiceCopyBegin', [$this]);
$Handler = Handler::getInstance();
$Factory = Factory::getInstance();

Henning Leutz
committed
QUI::getEvents()->fireEvent('quiqqerInvoiceCopy', [$this]);
if (empty($globalProcessId)) {
$globalProcessId = QUI\Utils\Uuid::get();

Henning Leutz
committed
}
if ($this->getAttribute('invoice_address')) {
try {
$address = json_decode($this->getAttribute('invoice_address'), true);
} catch (\Exception $Exception) {
QUI\System\Log::addDebug($Exception->getMessage());
}
}
QUI::getDataBase()->update(
$Handler->temporaryInvoiceTable(),
'type' => QUI\ERP\Constants::TYPE_INVOICE_TEMPORARY,
'customer_id' => $currentData['customer_id'],
'contact_person' => $currentData['contact_person'],
'invoice_address_id' => $invoiceAddressId,
'invoice_address' => $invoiceAddress,
'delivery_address' => $currentData['delivery_address'],
'order_id' => $currentData['order_id'],
'project_name' => $currentData['project_name'],
'payment_method' => $currentData['payment_method'],
'payment_data' => '',
'payment_time' => $currentData['payment_time'],
'time_for_payment' => (int)Settings::getInstance()->get('invoice', 'time_for_payment'),
'paid_status' => QUI\ERP\Constants::PAYMENT_STATUS_OPEN,
'paid_date' => null,
'paid_data' => null,
'date' => date('Y-m-d H:i:s'),
'data' => $currentData['data'],
'additional_invoice_text' => $currentData['additional_invoice_text'],
'articles' => $currentData['articles'],
'history' => '',
'comments' => '',
'customer_data' => $currentData['customer_data'],
'isbrutto' => $currentData['isbrutto'],
'currency_data' => $currentData['currency_data'],
'currency' => $currentData['currency'],
'nettosum' => $currentData['nettosum'],
'nettosubsum' => $currentData['nettosubsum'],
'subsum' => $currentData['subsum'],
'sum' => $currentData['sum'],
'vat_array' => $currentData['vat_array'],
'processing_status' => null
$NewTemporaryInvoice = $Handler->getTemporaryInvoice($New->getUUID());
QUI::getEvents()->fireEvent('quiqqerInvoiceCopyEnd', [
$this,
$NewTemporaryInvoice
]);
return $NewTemporaryInvoice;
* Create a credit note, set the invoice to credit note
* - Gutschrift
*
* @param null|QUI\Interfaces\Users\User $PermissionUser

Henning Leutz
committed
* @param bool|string $globalProcessId
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function createCreditNote(
QUI\Interfaces\Users\User $PermissionUser = null
// a credit node cant create a credit note
if ($this->getInvoiceType() === QUI\ERP\Constants::TYPE_INVOICE_CREDIT_NOTE) {
throw new Exception([
'quiqqer/invoice',
'exception.credit.note.cant.create.credit.note'
]);
}
'quiqqer.invoice.createCreditNote',
$PermissionUser
);
QUI::getEvents()->fireEvent(
'quiqqerInvoiceCreateCreditNote',
$Copy = $this->copy(QUI::getUsers()->getSystemUser(), $this->getGlobalProcessId());
$articles = $Copy->getArticles()->getArticles();
// change all prices
$ArticleList = $Copy->getArticles();
$ArticleList->clear();
$ArticleList->setCurrency($this->getCurrency());
$priceKeyList = [
'price',
'basisPrice',
'nettoPriceNotRounded',
'sum',
'nettoSum',
'nettoSubSum',
'nettoPrice',
'nettoBasisPrice',
'unitPrice',
];
$article = $Article->toArray();
foreach ($priceKeyList as $priceKey) {
if (isset($article[$priceKey])) {
$article[$priceKey] = $article[$priceKey] * -1;
}
}
if (isset($article['calculated'])) {
foreach ($priceKeyList as $priceKey) {
if (isset($article['calculated'][$priceKey])) {
$article['calculated'][$priceKey] = $article['calculated'][$priceKey] * -1;
}
}
}
if (isset($article['calculated']['vatArray'])) {
$article['calculated']['vatArray']['sum'] = $article['calculated']['vatArray']['sum'] * -1;
}
$Clone = new QUI\ERP\Accounting\Article($article);
$ArticleList->addArticle($Clone);
}
$PriceFactors = $ArticleList->getPriceFactors();
/* @var $PriceFactor QUI\ERP\Accounting\PriceFactors\Factor */
foreach ($PriceFactors as $PriceFactor) {
$PriceFactor->setNettoSum($PriceFactor->getNettoSum() * -1);
$PriceFactor->setSum($PriceFactor->getSum() * -1);
$PriceFactor->setValue($PriceFactor->getValue() * -1);
$PriceFactor->setNettoSumFormatted($Currency->format($PriceFactor->getNettoSum()));
$PriceFactor->setSumFormatted($Currency->format($PriceFactor->getSum()));
$PriceFactor->setValueText($Currency->format($PriceFactor->getValue()));
}
QUI::getLocale()->get('quiqqer/invoice', 'message.create.credit.from', [
'invoiceParentId' => $this->getUUID(),
'invoiceId' => $this->getUUID()
// credit note extra text
$localeCode = QUI::getLocale()->getLocalesByLang(
QUI::getLocale()->getCurrent()
);
);
$currentDate = $this->getAttribute('date');