Newer
Older
/**
* This file contains QUI\ERP\Accounting\Invoice\Invoice
*/
use QUI\Permissions\Permission;
use QUI\ERP\Money\Price;
use QUI\ERP\Accounting\ArticleListUnique;
use QUI\ERP\Accounting\Payments\Transactions\Transaction;
use QUI\ERP\Accounting\Invoice\Utils\Invoice as InvoiceUtils;
use QUI\ERP\Output\Output as ERPOutput;
* - This class present a posted invoice
*
* @package QUI\ERP\Accounting\Invoice
*/
class Invoice extends QUI\QDOM
{
/* @deprecated */
const PAYMENT_STATUS_OPEN = QUI\ERP\Constants::PAYMENT_STATUS_OPEN;
/* @deprecated */
const PAYMENT_STATUS_PAID = QUI\ERP\Constants::PAYMENT_STATUS_PAID;
/* @deprecated */
const PAYMENT_STATUS_PART = QUI\ERP\Constants::PAYMENT_STATUS_PART;
/* @deprecated */
const PAYMENT_STATUS_ERROR = QUI\ERP\Constants::PAYMENT_STATUS_ERROR;
/* @deprecated */
const PAYMENT_STATUS_CANCELED = QUI\ERP\Constants::PAYMENT_STATUS_CANCELED;
/* @deprecated */
const PAYMENT_STATUS_DEBIT = QUI\ERP\Constants::PAYMENT_STATUS_DEBIT;
// const PAYMENT_STATUS_CANCEL = 3;
// const PAYMENT_STATUS_STORNO = 3; // Alias for cancel
// const PAYMENT_STATUS_CREATE_CREDIT = 5;
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
*/
protected $type;
/**
* @var int
*/
protected $id;

Henning Leutz
committed
/**
* @var string
*/
protected $globalProcessId;
/**
* variable data for developers
*
* @var array
*/
protected $customData = [];
/**
* @var array
*/
protected $paymentData = [];
/**
* @var null|integer
*/
protected $Shipping = null;
/**
* Invoice constructor.
*
* @param $id
* @param Handler $Handler
*/
public function __construct($id, Handler $Handler)
{
$this->setAttributes($Handler->getInvoiceData($id));
$this->prefix = $this->getAttribute('id_prefix');
if ($this->prefix === false) {
$this->prefix = Settings::getInstance()->getInvoicePrefix();
}
$this->id = (int)\str_replace($this->prefix, '', $id);
switch ((int)$this->getAttribute('type')) {
case Handler::TYPE_INVOICE:
case Handler::TYPE_INVOICE_TEMPORARY:
case Handler::TYPE_INVOICE_CREDIT_NOTE:
case Handler::TYPE_INVOICE_REVERSAL:
case Handler::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);
}

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'));
$paymentData = \json_decode($paymentData, true);
$this->paymentData = $paymentData;
}
}
// shipping
if ($this->getAttribute('shipping_id')) {
$shippingData = $this->getAttribute('shipping_data');
$shippingData = \json_decode($shippingData, true);
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 = \json_decode($invoiceAddress, true);
$invoiceAddress['contactPerson'] = $this->getAttribute('contact_person');
$this->setAttribute('invoice_address', \json_encode($invoiceAddress));
}
}
/**
* Return the invoice id
*
* @return string
*/
/**
* Return the hash
* (Vorgangsnummer)
*
* @return 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 (int)\str_replace($this->prefix, '', $this->getId());
/**
* Return the invoice view
*
* @return InvoiceView
{
return new InvoiceView($this);
}
/**
* Return the unique article list
*
* @return ArticleListUnique
public function getArticles(): ArticleListUnique
$articles = $this->getAttribute('articles');
if (\is_string($articles)) {
$articles = \json_decode($articles, true);
try {
$articles['calculations']['currencyData'] = $this->getCurrency()->toArray();
} catch (QUI\Exception $Exception) {
QUI\System\Log::writeDebugException($Exception);
}

Henning Leutz
committed
$List = new ArticleListUnique($articles, $this->getCustomer());
$List->setLocale($this->getCustomer()->getLocale());
return $List;
/**
* 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'])) {
}
return QUI\ERP\Currency\Handler::getCurrency($currency['code']);
}

Henning Leutz
committed
/**
* @return QUI\ERP\User

Henning Leutz
committed
*/

Henning Leutz
committed
{
$invoiceAddress = $this->getAttribute('invoice_address');
$customerData = $this->getAttribute('customer_data');
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');
}

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

Henning Leutz
committed
*/

Henning Leutz
committed
{

Henning Leutz
committed
'country' => '',
'username' => '',
'firstname' => '',
'lastname' => $this->getAttribute('editor_name'),

Henning Leutz
committed
'lang' => '',

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);

Henning Leutz
committed
// the status is another as calced, to we must update the invoice data
if ($this->getAttribute('paid_status') !== $oldStatus) {
$this->calculatePayments();
}
if ($this->getInvoiceType() === Handler::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)
if (\array_key_exists($key, $this->paymentData)) {
return $this->paymentData[$key];
}
return null;
}
/**
* ALIAS for $this->getPaymentDataEntry to be consistent with InvoiceTemporary.
*
* @param string $key
* @return mixed|null
*/
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();
if ($Payment->refundSupport()) {
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->getId().'. 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()
{
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
return (int)$this->type;
}
/**
* Cancel the invoice
* (Reversal, Storno, Cancel)
*
* @param null|QUI\Interfaces\Users\User $PermissionUser
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function reversal(string $reason, $PermissionUser = null): int
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',
'username' => $User->getName(),
'uid' => $User->getId()

Henning Leutz
committed
$CreditNote = $this->createCreditNote(
QUI::getUsers()->getSystemUser(),
$this->getGlobalProcessId()
);
$CreditNote->setInvoiceType(Handler::TYPE_INVOICE_REVERSAL);
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
// Cancellation invoice extra text
$localeCode = QUI::getLocale()->getLocalesByLang(
QUI::getLocale()->getCurrent()
);
$Formatter = new \IntlDateFormatter(
$localeCode[0],
\IntlDateFormatter::SHORT,
\IntlDateFormatter::NONE
);
$currentDate = $this->getAttribute('date');
if (!$currentDate) {
$currentDate = \time();
} else {
$currentDate = \strtotime($currentDate);
}
$message = $this->getCustomer()->getLocale()->get(
'quiqqer/invoice',
'message.invoice.cancellationInvoice.additionalInvoiceText',
[
'id' => $this->getId(),
'date' => $Formatter->format($currentDate)
]
);
if (empty($message)) {
$message = '';
}
// saving copy
$CreditNote->setAttribute('additional_invoice_text', $message);
$CreditNote->save(QUI::getUsers()->getSystemUser());
try {
Permission::checkPermission('quiqqer.invoice.canEditCancelInvoice', $PermissionUser);
} catch (QUI\Exception $Exception) {
$CreditNote->post(QUI::getUsers()->getSystemUser());
}
$this->addHistory(
QUI::getLocale()->get(
'quiqqer/invoice',
'history.message.reversal.created',
'username' => $User->getName(),
'uid' => $User->getId(),
'creditNoteId' => $CreditNote->getId()
// set the invoice status
$this->type = Handler::TYPE_INVOICE_CANCEL;
$CreditNote = $CreditNote->post(QUI::getUsers()->getSystemUser());
$this->data['canceledId'] = $CreditNote->getCleanId();
QUI::getDataBase()->update(
Handler::getInstance()->invoiceTable(),
'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
* @return int - ID of the new
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function cancellation(string $reason, $PermissionUser = null): int
return $this->reversal($reason, $PermissionUser);
}
/**
* Alias for reversal
*
* @param string $reason
* @param null|QUI\Interfaces\Users\User $PermissionUser
* @return int - ID of the new
*
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function storno(string $reason, $PermissionUser = null): int
return $this->reversal($reason, $PermissionUser);
}
/**
* Copy the invoice to a temporary invoice
*
* @param null|QUI\Interfaces\Users\User $PermissionUser

Henning Leutz
committed
* @param false|string $globalProcessId
* @throws Exception
* @throws QUI\Exception
* @throws QUI\Permissions\Exception
public function copy($PermissionUser = null, $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',
'username' => $User->getName(),
'uid' => $User->getId()

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

Henning Leutz
committed
QUI::getEvents()->fireEvent('quiqqerInvoiceCopy', [$this]);
if (empty($globalProcessId)) {
$globalProcessId = $this->getHash();
}
// Invoice Address
$invoiceAddressId = '';
$invoiceAddress = '';
if ($this->getAttribute('invoice_address')) {
try {
$address = \json_decode($this->getAttribute('invoice_address'), true);
$Address = new QUI\ERP\Address($address);
$invoiceAddressId = $Address->getId();
$invoiceAddress = $Address->toJSON();
} catch (\Exception $Exception) {
QUI\System\Log::addDebug($Exception->getMessage());
}
}
QUI::getDataBase()->update(
$Handler->temporaryInvoiceTable(),

Henning Leutz
committed
'global_process_id' => $globalProcessId,
'type' => Handler::TYPE_INVOICE_TEMPORARY,
'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_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,
'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'],
'nettosum' => $currentData['nettosum'],
'nettosubsum' => $currentData['nettosubsum'],
'subsum' => $currentData['subsum'],
'sum' => $currentData['sum'],
'vat_array' => $currentData['vat_array'],
$NewTemporaryInvoice = $Handler->getTemporaryInvoice($New->getId());

Henning Leutz
committed
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($PermissionUser = null, $globalProcessId = false): InvoiceTemporary
// a credit node cant create a credit note
if ($this->getInvoiceType() === Handler::TYPE_INVOICE_CREDIT_NOTE) {
throw new Exception([
'quiqqer/invoice',
'exception.credit.note.cant.create.credit.note'
]);
}
'quiqqer.invoice.createCreditNote',
$PermissionUser
);
QUI::getEvents()->fireEvent(
'quiqqerInvoiceCreateCreditNote',

Henning Leutz
committed
$Copy = $this->copy(QUI::getUsers()->getSystemUser(), $globalProcessId);
$articles = $Copy->getArticles()->getArticles();
// change all prices
$ArticleList = $Copy->getArticles();
$ArticleList->clear();
$ArticleList->setCurrency($this->getCurrency());
foreach ($articles as $Article) {
$article = $Article->toArray();
$article['unitPrice'] = $article['unitPrice'] * -1;
$Clone = new QUI\ERP\Accounting\Article($article);
$ArticleList->addArticle($Clone);
}
$PriceFactors = $ArticleList->getPriceFactors();
$Currency = $this->getCurrency();
/* @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->getId(),
'invoiceId' => $this->getId()
// credit note extra text
$localeCode = QUI::getLocale()->getLocalesByLang(
QUI::getLocale()->getCurrent()
);
$Formatter = new \IntlDateFormatter(
$localeCode[0],
\IntlDateFormatter::SHORT,
\IntlDateFormatter::NONE
);
$currentDate = $this->getAttribute('date');
if (!$currentDate) {
$message = $this->getCustomer()->getLocale()->get(
'quiqqer/invoice',
'message.invoice.creditNote.additionalInvoiceText',
'id' => $this->getId(),
'date' => $Formatter->format($currentDate)
);
$additionalText = $Copy->getAttribute('additional_invoice_text');
if (!empty($additionalText)) {
$additionalText .= '<br />';
}
$additionalText .= $message;
// saving copy
$Copy->setData('originalId', $this->getCleanId());
$Copy->setData('originalIdPrefixed', $this->getId());
$Copy->setAttribute('date', \date('Y-m-d H:i:s'));
$Copy->setAttribute('additional_invoice_text', $additionalText);
$Copy->setAttribute('currency_data', $this->getAttribute('currency_data'));
$Copy->setInvoiceType(Handler::TYPE_INVOICE_CREDIT_NOTE);
if ($this->getAttribute('invoice_address')) {
try {
$address = \json_decode($this->getAttribute('invoice_address'), true);
$Address = new QUI\ERP\Address($address);
$invoiceAddressId = $Address->getId();
$invoiceAddress = $Address->toJSON();
$Copy->setAttribute('invoice_address_id', $invoiceAddressId);
$Copy->setAttribute('invoice_address', $invoiceAddress);
} catch (\Exception $Exception) {
QUI\System\Log::addDebug($Exception->getMessage());
}
}
$Copy->save(QUI::getUsers()->getSystemUser());
QUI::getLocale()->get('quiqqer/invoice', 'message.create.credit', [
'invoiceParentId' => $this->getId(),
'invoiceId' => $Copy->getId()
$CreditNote = Handler::getInstance()->getTemporaryInvoice($Copy->getId());
QUI::getEvents()->fireEvent(
'quiqqerInvoiceCreateCreditNoteEnd',
[$this, $CreditNote]
);
return $CreditNote;
/**
* @param Transaction $Transaction
*/
public function addTransaction(Transaction $Transaction)
{
QUI\ERP\Debug::getInstance()->log('Invoice:: add transaction');
if ($this->getHash() !== $Transaction->getHash()) {
return;
}
$currentPaidStatus = $this->getAttribute('paid_status');
if ($this->getInvoiceType() == Handler::TYPE_INVOICE_REVERSAL
|| $this->getInvoiceType() == Handler::TYPE_INVOICE_CANCEL
|| $this->getInvoiceType() == Handler::TYPE_INVOICE_CREDIT_NOTE
) {
return;
}
if ($currentPaidStatus === $this->getAttribute('paid_status')
&& ($this->getAttribute('paid_status') == QUI\ERP\Constants::PAYMENT_STATUS_PAID ||
$this->getAttribute('paid_status') == QUI\ERP\Constants::PAYMENT_STATUS_CANCELED)
) {
return;
}
QUI\ERP\Debug::getInstance()->log('Order:: add transaction start');
$User = QUI::getUserBySession();
$paidData = $this->getAttribute('paid_data');
$amount = Price::validatePrice($Transaction->getAmount());
$date = $Transaction->getDate();
QUI::getEvents()->fireEvent(
'quiqqerInvoiceAddTransactionBegin',
);
if (!$amount) {
return;
}
if (!\is_array($paidData)) {
$paidData = \json_decode($paidData, true);
}
if ($date === false) {
$date = time();
}

Patrick Müller
committed
$isTxAlreadyAdded = function ($txid, $paidData) {