<?php

// TODO: pe insert si update sa setez userul manual (poate fac asta din cronjob)

// TODO: [not important] inlocuit trigger-ul "EntityMtFinishOperation-[Document]" cu cel din parinte: EntityExtension-[Document]
// TODO: [not important] - in nume fisier sa pun timestamp + milisecunde, ca sa pot sa scot sleep(1) de la generarea documentului

// TODO: [not important] - langurile obiectelor speciale sa se incarce dintr-un dir custom load (pentru a putea fi incarcate pentru obiectele care sunt extinse din altele - vezi de ex: invoiceStandardTablePositiveVals.

namespace Entity;

class Document extends \App\Entity\GeneralMtAbstract
{

	/**
	 * Indicate if the operations available for the current entity must be done inside a transaction or not.
	 * @var Boolean
	 */
	protected $saveInsideTransaction = true;


	protected $addAndUpdateDateTimeFormat = 'timestamp'; // TODO: to be eliminated after the table columns are renamed and replaced with datetime

	protected $convertNullsToBlanks = true;

	/**
	 * Indicates if the pivot was voided before this document generation.
	 * @var Boolean
	 */
	private $pivotIsVoided;

	/**
	 * The inserting document date.
	 * @var String (ISO date)
	 */
	private $documentDate;

	/**
	 * Indicates if this document saving will use a previous number or it will generate a new one.
	 * @var Boolean
	 */
	private $newNumber=null;

	/**
	 * The generated document number.
	 * @var Integer
	 */
	private $documentNumber;

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralMtAbstract::setTableGroup()
	 */
	protected function setTableGroup(): void
	{
		global $DBTDocuments;
		$this->tableGroup=$DBTDocuments;
	}


	/**
	 * Static method for deleting a document - will not be developed.
	 * @throws \Exception
	 */
	static public function delete($id)
	{
		throw new \Exception(__METHOD__.' will NOT be developed.');
	}

	/**
	 * Static method for deleting a document file
	 * @throws \Exception
	 */
	static public function deleteFile($id)
	{
		return new self('deleteFile', $id);
	}

	/**
	 * Static method for sending a document via email.
	 * @param Int $id
	 */
	static public function sendMail($id)
	{
		return new self('sendMail', $id);
	}

	/**
	 * Static method for updating history attribute when download/view/print a document
	 * @param Int $id
	 * @return \Entity\Document
	 */
	static public function openDocument($id)
	{
		return new self('openDocument', $id);
	}


	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\SelectAbstract::getAttribute()
	 */
	protected function getAttribute($var)
	{

		// CHECKS EACH FIELD THAT CAN BE EXTRACTED FOR THIS INVOICE ///////////////////////////////
		switch ($var) {
			case 'idPivot': case 'idTemplate': case 'format': case 'docNumber': case 'docDate':
			case 'docFile': case 'comments': case 'preview': case 'emailSent': case 'disableEmailSending': {
				return $this->fields[$var];
			}

			// id auto process rule and the document series id
			case 'idAutoProcessRule': case 'idDocumentSeries': {
				if (!array_key_exists($var, $this->fields)) {
					$ei=$this->__get('extraInfo');
					$this->fields['idAutoProcessRule']=(Int)$ei['idAutoProcessRule'];
					$this->fields['idDocumentSeries']=(Int)$ei['idDocumentSeries'];
				}
				return $this->fields[$var];
			}

			case 'documentSeries': {
				return \Entity\DocumentSeries::select($this->__get('idDocumentSeries'));
			}

			case 'autoProcessRule': {
				return \Entity\AutoProcessRule::select($this->__get('idAutoProcessRule'));
			}

			// the pivot code (order, service, etc)
			case 'pivotCode': {
				$this->fields['pivotCode']=$this->fields['pivot'];
				return $this->fields['pivotCode'];
			}

			// pivot object (reservation/reservationService/invoice/invoiceCashing/cashBookDoc/balance)
			case 'pivot': {
				if ($this->fields['pivot']=='order') 		return Order::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='service') 	return Service::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='invoice') 	return Invoice::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='cashing') 	return InvoiceCashing::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='payment') 	return SupplierInvoicePayment::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='balance') return Balance::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='cashBookDoc') return CashBookDocument::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='importedService') return ImportedService::select($this->fields['idPivot'], $this->selectForUpdate);
				elseif ($this->fields['pivot']=='importedDocument') return ImportedDocument::select($this->fields['idPivot'], $this->selectForUpdate);
			}

			// document type
			case 'docTypeCode': {
				$this->fields['docTypeCode']=$this->fields['docType'];
				return $this->fields['docTypeCode'];
			}
			case 'docTypeName': {
				if (!array_key_exists('docTypeName', $this->fields)) {
					$this->fields['docTypeName'] = \App\Config\IssueDocuments::getInstance()->getDocTypeName($this->fields['docType']);
				}
				return $this->fields['docTypeName'];
			}

			// document template
			case 'template': {
				return \Entity\DocumentTemplate::select($this->fields['idTemplate'], $this->selectForUpdate);
			}

			// generation code
			case 'generationCode': {
				$this->fields['generationCode']=$this->fields['generation'];
				return $this->fields['generationCode'];
			}

			// fieldsDetails / history
			case 'fieldsDetails': case 'history': case 'extraInfo': {
				if (!is_array($this->fields[$var])) {
					if ($this->fields[$var]) {
						$this->fields[$var] = unserialize($this->fields[$var]);
						if (!$this->fields[$var]) {
							throw new \Exception("Invalid field stored as serialized text ({$var}) found for document #".$this->id);
						}
					} else {
						$this->fields[$var] = [];
					}
				}
				return $this->fields[$var];
			}

			case 'customFieldsValues': case 'standardFieldsValues':
				if (!array_key_exists($var, $this->fields)) {
					$this->fields['customFieldsValues'] = [];
					$this->fields['standardFieldsValues'] = [];

					foreach($this->__get('fieldsDetails') as $fieldId=>$fieldDetail) {
						if (preg_match("/^customText[0-9]{2}$/", $fieldId)) {
							$this->fields['customFieldsValues'][$fieldId] = $fieldDetail['val'];
						}
						else {
							$this->fields['standardFieldsValues'][$fieldId] = $fieldDetail['val'];
						}
					}
				}

				return $this->fields[$var];
			break;

			// the cleaned file name
			case 'docFileClean': {
				if (!$this->fields[$var]) {
					$p1 = mb_strrpos($this->fields['docFile'], '[');
					$p2 = mb_strrpos($this->fields['docFile'], ']');
					if ($p1!==false and $p2!==false) {
						$this->fields[$var] = mb_substr($this->fields['docFile'], 0, $p1) . mb_substr($this->fields['docFile'], $p2 + 1);
					} else {
						$this->fields[$var] = $this->fields['docFile'];
					}
				}
				return $this->fields[$var];
			}


			// document file path
			case 'docFilePath': {
				if (!$this->fields[$var]) {
					$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
					$this->fields[$var]=MODULE_REL_PATH.$CFG['paths']['generatedDocuments'].$this->getTableMonth().'/'.$this->__get('pivotCode').'/'.$this->__get('idPivot').'/';
				}
				return $this->fields[$var];
			}

			case 'docFileNameWithPath': {
				if (!is_array($this->fields[$var])) {
					$this->fields[$var]=$this->__get('docFilePath').$this->fields['docFile'];
				}
				return $this->fields[$var];
			}

			// document file content
			case 'docFileContent': {
				if (!is_array($this->fields[$var])) {
					$file=$this->__get('docFileNameWithPath');
					if (file_exists($file)) {
						$this->fields[$var]=file_get_contents($file);
					} else {
						$this->fields[$var]=null;
					}
				}
				return $this->fields[$var];
			}

			// document file size
			case 'docFileSize': {
				if (!is_array($this->fields[$var])) {
					$file=$this->__get('docFileNameWithPath');
					if (\Engine\File::check($file)) {
						$this->fields[$var]=filesize($file);
					} else {
						$this->fields[$var]=null;
					}
				}
				return $this->fields[$var];
			}

			// printed - indicates if the document is printed or not
			case 'printed': {
				if (!array_key_exists('printed', $this->fields)) {
					// force obtaining the document history
					$thisHistory=$this->__get('history');
					$docPrinted=false;
					foreach($thisHistory as $item) {
						if ($item['op']=="p") {
							$docPrinted=true;
							break;
						}
					}
					$this->fields['printed']=$docPrinted;
				}
				return $this->fields['printed'];
			}

			// indicates if the current document is a preview
			case 'voided': {
				if (!array_key_exists($var, $this->fields)) {
					// force obtaining the document extraInfo
					$ei=$this->__get('extraInfo');
					$this->fields['voided']=(bool)$ei['voided'];
				}
				return $this->fields[$var];
			}

			// returns the list of services if the pivot is an order
			case 'servicesList': {
				if (!array_key_exists($var, $this->fields)) {
					// force obtaining the document extraInfo
					$ei=$this->__get('extraInfo');
					$this->fields['servicesList']=(Array)$ei['servicesList'];
				}
				return $this->fields[$var];
			}

			case 'addDateTime': case 'idAddUser': {
				if (!array_key_exists($var, $this->fields)) {
					foreach($this->__get('history') as $hisItem) {
						if ($hisItem['op']=='g') {
							$this->fields['addDateTime'] = \Engine\DateTime::createFromTimestamp($hisItem['t'])->formatDatetimeIso();
							$this->fields['idAddUser'] = $hisItem['us'];
							break;
						}
					}
				}
				return $this->fields[$var];
			}

			case 'emailDetails': {
				if (!array_key_exists($var, $this->fields)) {
					// force obtaining the document history
					$thisHistory=$this->__get('history');

					$emailDetails = [];
					foreach(array_reverse($thisHistory) as $item) {
						if ($item['op']=="e") {
							$emailDetails = $item['details'];
							$emailDetails['time'] = $item['t'];
						}
					}
					$this->fields[$var]=$emailDetails;
				}

				return $this->fields[$var];
			}

			case 'generatedHistory': {
				if (!array_key_exists($var, $this->fields)) {
					// force obtaining the document history
					$thisHistory=$this->__get('history');

					$this->fields[$var]=reset($thisHistory);
				}

				return $this->fields[$var];
			}

			// $var IS NOT VALID - throws Exception -----------------------------------------------
			default: {
				return parent::getAttribute($var);
			}

		}

	}

	/**
	 * Generates the default email subject for a 'sendMail' operation.
	 * @param Int $idUser
	 * @throws \Exception - for invalid operation
	 * @return String
	 */
	public function getSendMailDefaultSubject($idUser = CURRENT_ID_USER)
	{
		// check operation
		if ($this->operation!='select') {
			throw new \Exception('You can call '.__METHOD__.' only for `select` operations.');
		}

		$pivotCode = $this->__get('pivotCode');

		// get default subject from user preferences
		$fieldSubj="docEmail_{$pivotCode}_defaultSubject";
		$defaultSubject=\App\EntityPreference\User::getInstance($idUser)->$fieldSubj;

		// get default subject from agency configs
		if (!$defaultSubject) {
			$agencyDocumentsEmailDataObject = \App\Config\AgencyDocumentsEmail::getInstance();
			$defaultSubject = $agencyDocumentsEmailDataObject->getSubject($pivotCode);
		}

		if (!$defaultSubject) {
			$_LANG = \App\Loader_Lang::getInstance()->getGlobal();
			$defaultSubject=$_LANG['documentEmail']['defaultMailSubject'];
		}

		$defaultSubject = htmlUnclean($defaultSubject);
		// replace vars (if any)
		$textProcessor = new \Engine\GenDocument\TextProcessor\TextProcessor(new \Engine\GenDocument\TextProcessor\DefaultTextHandler(), \App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->__get('pivotCode'), $this->fields['idPivot'], $this->__get('docTypeCode')));
		$textProcessor->reset();
		$defaultSubjectReplaced=$textProcessor->parse($defaultSubject);

		return $defaultSubjectReplaced;
	}

	/**
	 * Generates the default email body for a 'sendMail' operation.
	 * @param Int $idUser
	 * @throws \Exception - for invalid operation
	 * @return String
	 */
	public function getSendMailDefaultBody($idUser = CURRENT_ID_USER)
	{
		// check operation
		if ($this->operation!='select') {
			throw new \Exception('You can call '.__METHOD__.' only for `select` operations.');
		}
		// get default subject from
		$pivotCode = $this->__get('pivotCode');
		$fieldSubj="docEmail_{$pivotCode}_defaultBody";

		$defaultBody=\App\EntityPreference\User::getInstance($idUser)->$fieldSubj;

		// get default body from agency configs
		if (!$defaultBody) {
			$agencyDocumentsEmailDataObject = \App\Config\AgencyDocumentsEmail::getInstance();
			$defaultBody = $agencyDocumentsEmailDataObject->getBody($pivotCode);
		}

		if (!$defaultBody) {
			$_LANG = \App\Loader_Lang::getInstance()->getGlobal();
			$defaultBody = $_LANG['documentEmail']['defaultMailBody'];
		}
		$defaultBody=htmlUnclean($defaultBody);
		// replace vars (if any)
		$textProcessor = new \Engine\GenDocument\TextProcessor\TextProcessor(new \Engine\GenDocument\TextProcessor\DefaultTextHandler(), \App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->__get('pivotCode'), $this->fields['idPivot'], $this->__get('docTypeCode')));
		$textProcessor->reset();
		$defaultBodyReplaced=$textProcessor->parse($defaultBody);
		// set the body text
		return '<font face="Arial, Helvetica, sans-serif" size="2">'.$defaultBodyReplaced."<br />".User::select($idUser)->emailSignature.'</font>';
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::setAttribute()
	 */
	protected function setAttribute($var, $value)
	{
		// set attributes for 'insert' operation
		if ($this->operation=='insert') {
			return $this->setAttribute_insert($var, $value);
		}
		// set attributes for 'update' operation
		elseif ($this->operation=='update') {
			return $this->setAttribute_update($var, $value);
		}
		// set attributes for 'sendMail' operation
		elseif ($this->operation=='sendMail') {
			return $this->setAttribute_sendMail($var, $value);
		}
		// set attributes for 'openDocument' operation
		elseif ($this->operation == 'openDocument') {
			return $this->setAttribute_openDocument($var, $value);
		}
		// all other operations do not support this method
		else {
			throw new \Exception("You cannot use __set magic method with `{$this->operation}` operation in `".__CLASS__."` class.");
		}
	}

	/**
	 * Sets an attribute for 'insert' operation
	 * @param String $var
	 * @param Mixed $value
	 * @throws \Exception - if a bat attribute name or an invalid value is set
	 */
	private function setAttribute_insert($var, $value)
	{

		switch ($var) {

			// simple fields
			case 'idPivot': case 'docType': case 'idTemplate': case 'format': case 'docNumber': case 'docDate':
			case 'comments': case 'docFile': case 'docContent': case 'idDocumentSeries': case 'preview': case 'idIssueUser':
			case 'idAutoProcessRule': case 'disableEmailSending': {
				$this->fields[$var]=$value;
				break;
			}

			// check pivot type
			case 'pivot': {
				if (!in_array($value, ['order','service','invoice','cashing', 'payment', 'cashBookDoc', 'importedService', 'importedDocument', 'balance'])) throw new \Exception('The pivot is not valid.');
				$this->fields[$var]=$value;
				break;
			}

			// doc generation type
			case 'generation': {
				if ($value!='manual' and $value!='auto') throw new \Exception('The `generation` can only be `auto` or `manual`.');
				$this->fields[$var]=$value;
				break;
			}

			// standard/custom fields values - associative arrays of field=>value
			case 'standardFieldsValues': case 'customFieldsValues':
			case 'servicesList': { 			// stores the list of services - available only for order pivot
				if (!is_array($value)) throw new \Exception("The `{$var}` field should be set as an array for `".__METHOD__."` method.");
				$this->fields[$var]=$value;
				break;
			}

			// extra info - updates only the set keys
			case 'extraInfo': {
				if (!is_array($value)) {
					throw new \Exception("The field `extraInfo` can only be set as an array for the `".__CLASS__."` entity.");
				}
				$this->fields[$var]=$value;
				break;
			}

			// $var IS NOT VALID - throws Exception -----------------------------------------------
			default: {
				throw new \Exception("Wrong var ({$var}) for the '".__CLASS__."' class, operation=`{$this->operation}`.");
			}

		}

	}


	/**
	 * Sets an attribute for the 'update' operation.
	 * @param String $var
	 * @param Mixed $value
	 * @throws \Exception - if a bad attribute is set.
	 */
	private function setAttribute_update($var, $value)
	{

		switch ($var) {

			// simple fields
			case 'comments': case 'docFileContent': {
				$this->fields[$var]=$value;
				break;
			}

			// extra info - updates only the set keys
			case 'extraInfo': {
				if (!is_array($value)) {
					throw new \Exception("The field `extraInfo` can only be set as an array for the `".__CLASS__."` entity.");
				}
				$this->fields[$var]=$value;
				break;
			}

			// $var IS NOT VALID - throws Exception -----------------------------------------------
			default: {
				throw new \Exception("Wrong var ({$var}) for the '".__CLASS__."' class, operation=`{$this->operation}`.");
			}

		}


	}


	/**
	 * Sets an attribute for 'sendMail' operation
	 * @param String $var
	 * @param Mixed $value
	 * @throws \Exception - if a bad attribute name or an invalid value is set
	 */
	private function setAttribute_sendMail($var, $value)
	{

		switch ($var) {
			// simple fields
			case 'selfBcc': case 'sendReadReceipt': case 'subject': case 'body':
			case 'idSenderUser': case 'forceSenderEmail': case 'forceSenderName': case 'attachmentCalendar':
			{
				$this->fields[$var]=$value;
				break;
			}

			// array fields
			case 'receiversTo': case 'receiversCc': case 'uploadFiles': case 'generatedFiles': {
				if (!is_array($value)) throw new \Exception("The field `{$var}` can only be set as an array.");
				$this->fields[$var]=$value;
				break;
			}

			// $var IS NOT VALID - throws Exception -----------------------------------------------
			default: {
				throw new \Exception("Wrong var ({$var}) for the '".__CLASS__."' class, operation=`{$this->operation}`.");
			}
		}

	}

	/**
	 * Sets an attribute for 'openDocument' operation
	 * @param String $var
	 * @param Mixed $value
	 * @throws \Exception - if a bad attribute name or an invalid value is set
	 */
	private function setAttribute_openDocument($var, $value)
	{
		switch ($var) {
			// simple fields
			case 'action':
			{
				$this->fields[$var]=$value;
				break;
			}

			// $var IS NOT VALID - throws Exception -----------------------------------------------
			default: {
				throw new \Exception("Wrong var ({$var}) for the '".__CLASS__."' class, operation=`{$this->operation}`.");
			}
		}
	}

	/**
	 * Returns the pivot object (order, service, etc) for this insert-ing pivot - it selects "forUpdate" it.
	 * @return - the EntityMt pivot
	 */
	public function getInsertingDocumentPivot()
	{
		// check inputs
		if ($this->operation!='insert') throw new \Exception('The method `'.__METHOD__.'` can only be used on `insert` operation.');
		if (!$this->fields['pivot']) throw new \Exception('The method `'.__METHOD__.'` can only be called after the pivot was set.');
		if (!$this->fields['idPivot']) throw new \Exception('The method `'.__METHOD__.'` can only be called after the idPivot was set.');

		// get pivot details
		switch ($this->fields['pivot']) {
			case 'order': {
				return Order::selectForUpdate($this->fields['idPivot']);
			}
			case 'service': {
				return Service::selectForUpdate($this->fields['idPivot']);
			}
			case 'invoice': {
				return Invoice::selectForUpdate($this->fields['idPivot']);
			}
			case 'cashing': {
				return InvoiceCashing::selectForUpdate($this->fields['idPivot']);
			}
			case 'payment': {
				return SupplierInvoicePayment::selectForUpdate($this->fields['idPivot']);
			}
			case 'cashBookDoc': {
				return CashBookDocument::selectForUpdate($this->fields['idPivot']);
			}
			case 'importedService': {
				return ImportedService::selectForUpdate($this->fields['idPivot']);
			}
			case 'importedDocument': {
				return ImportedDocument::selectForUpdate($this->fields['idPivot']);
			}
			case 'balance': {
				return Balance::selectForUpdate($this->fields['idPivot']);
			}
			default: {
				throw new \Exception('Wrong pivot in `'.__METHOD__.'` method call.');
			}
		}

	}


	/**
	 * Returns the document date for a certain document type upon inserting.
	 * @return String - the document date
	 */
	public function getDocumentDate()
	{

		// if document date was already saved inside the object / otherwise determine it
		if ($this->documentDate) return $this->documentDate;

		// CHECK INPUT ////////////////////////////////////////////////////////////////////////////
		// check that operation is okay
		if ($this->operation!='insert') throw new \Exception("You can use ".__METHOD__." only for `insert` operation.");

		// pivot type must be set
		if (!$this->fields['pivot']) throw new \Exception("You can't use ".__METHOD__." because you didn't set the document pivot.");

		// document type must be set
		if (!$this->fields['docType']) throw new \Exception("You can't use ".__METHOD__." because you didn't set the document type.");

		$docCfgObj = \App\Config\IssueDocuments::getInstance();

		// if the document date is set for today - return 'today'
		if ($docCfgObj->getDocDate($this->fields['docType'])=='today') {
			$return=TODAY_ISO;
		}
		// get the date from manual input
		elseif ($docCfgObj->getDocDate($this->fields['docType'])=='manual') {
			$return=$this->fields['docDate'];
		}
		// get the date from the pivot (works only for invoice and cashing)
		elseif ($docCfgObj->getDocDate($this->fields['docType'])=='pivotDate') {
			// check that the pivot id is set
			if (!$this->fields['idPivot']) {
				throw new \Exception(sprintf("[%s] The pivot id for this document is not correct (%s)", __METHOD__, $this->fields['idPivot']));
			}

			switch ($this->fields['pivot']) {
				case 'invoice':
					$return = \Entity\Invoice::selectForUpdate($this->fields['idPivot'])->invoiceDate;
				break;
				case 'cashing':
					$return = \Entity\InvoiceCashing::selectForUpdate($this->fields['idPivot'])->docDate;
				break;
				case 'payment':
					$return = \Entity\SupplierInvoicePayment::selectForUpdate($this->fields['idPivot'])->docDate;
				break;
				case 'cashBookDoc':
					$return = \Entity\CashBookDocument::selectForUpdate($this->fields['idPivot'])->date;
				break;
				case 'importedDocument':
					$return = \Entity\ImportedDocument::selectForUpdate($this->fields['idPivot'])->docDate;
				break;
				case 'balance':
					$return = \Entity\Balance::selectForUpdate($this->fields['idPivot'])->balanceDate;
				break;

				default:
					throw new \Exception(sprintf("[%s] The pivot type for this document is not correct (%s)", __METHOD__, $this->fields['pivot']));
			}
		}
		// invalid docDate config var
		else {
			throw new \Exception('The config var `docDate` is not valid for the `'.$this->fields['docType'].'` doc type.');
		}

		// cache the document date inside the object
		$this->documentDate=$return;
		// return the current value
		return $return;

	}


	/**
	 * Returns if the document received a new number or not.
	 * @return Boolean
	 */
	public function getNewNumberFlag()
	{
		return $this->newNumber;
	}



	/**
	 * For "insert" operation: sets the table month and sets the manual fields values.
	 * @see \App\Entity\GeneralAbstract::save_preCheck()
	 */
	protected function save_preCheck()
	{

		// INSERT OPERATION ///////////////////////////////////////////////////////////////////////
		if ($this->operation=='insert') {
			if (!$this->fields['idPivot']) {
				throw new \Exception('You did not set the idPivot for the inserting document.');
			}
			// set the current table month - depending on the pivot id
			if ($this->fields['pivot'] == 'cashBookDoc') {
				$this->setTableMonth($this->getAttribute('pivot')->date);
			}
			elseif ($this->fields['pivot'] == 'importedService') {
				$this->setTableMonth( \Engine\DateTime::createFromIsoDatetime($this->getAttribute('pivot')->addTime)->formatDateIso() );
			}
			elseif ($this->fields['pivot'] == 'importedDocument') {
				$this->setTableMonth( \Engine\DateTime::createFromIsoDatetime($this->getAttribute('pivot')->addTime)->formatDateIso() );
			}
			else {
				$this->setTableMonth($this->fields['idPivot']);
			}

			// rewrites the values for the standard/custom fields
			$this->save_preCheck_setManualFieldsValues();

			// make sure that documentDate attribute is set inside the object
	 		$this->getDocumentDate();

			// sets the `pivotIsVoided` flag
			$this->save_preCheck_setPivotIsVoidedFlag();

			// sets the `newNumber` flag
			$this->save_preCheck_setNewDocumentFlag();

		}

	}

	/**
	 * Saves the manual values set for standard and custom fields - they will later be checked in the document validator.
	 * It is executed only for insert operation
	 */
	private function save_preCheck_setManualFieldsValues()
	{
		if ($this->operation=='insert') {
			if ($this->fields['standardFieldsValues']) {
				foreach($this->fields['standardFieldsValues'] as $fld=>$val) {
					\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue($fld, $val);
				}
			}
			// if these were manually set
			if (array_key_exists('customFieldsValues', $this->fields)) {
				foreach($this->fields['customFieldsValues'] as $fld=>$val) {
					\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue($fld, $val);
				}
			}
			// if nothing was set - get the default values from the template
			else {
				if ($this->fields['idTemplate']) {
					$docTplCustFields = \Entity\DocumentTemplate::selectForUpdate($this->fields['idTemplate'])->customFieldsDetails;
					foreach($docTplCustFields as $fld=>$fldDet) {
						\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue($fld, $fldDet['value']);
					}
				}
			}

		}
	}

	/**
	 * Attaches `pivotIsVoided` flag to the object.
	 */
	private function save_preCheck_setPivotIsVoidedFlag()
	{

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}

		$this->pivotIsVoided=false;
		$pivotObj=$this->getInsertingDocumentPivot();
		switch($this->fields['pivot']) {
			case 'order': case 'service': {
				if ($pivotObj->statusOp=='XX') $this->pivotIsVoided=true;
				break;
			}
			case 'invoice': case 'cashing': {
				if ($pivotObj->voidTime) $this->pivotIsVoided=true;
				break;
			}
			case 'cashBookDoc': {
				if ($pivotObj->deleted) {
					$this->pivotIsVoided=true;
				}
				break;
			}
			case 'payment': {
				if ($pivotObj->deleted) {
					$this->pivotIsVoided=true;
				}
				break;
			}
			case 'importedService': {
				if (in_array($pivotObj->systemStatus, ['deleted','cancelled','refunded'])) {
					$this->pivotIsVoided=true;
				}
			}
			case 'importedDocument': {
				if ($pivotObj->systemStatus == 'cancelled') {
					$this->pivotIsVoided=true;
				}
			}
		}
	}


	/**
	 * Sets the internal newNumber flag value.
	 */
	private function save_preCheck_setNewDocumentFlag()
	{

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}

		// INITIALIZATIONS ////////////////////////////////////////////////////////////////////////

		// assume that newNumber is false - is rewritten to 'true' if it is the case
		$this->newNumber=false;

		// these values are stored as simple vars as it is easier to work with them like this
		// the current document type
		$documentType=$this->fields['docType'];
		// the current document series id
		$idDocumentSeries=$this->fields['idDocumentSeries'];
		// preview flag
		$preview=$this->fields['preview'];
		// the current pivot object
		$pivotObj=$this->getInsertingDocumentPivot();

		///////////////////////////////////////////////////////////////////////////////////////////
		// IF THE DOCUMENT IS AUTO-GENERATED //////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['generation']=='auto') {

			$docCfgObj = \App\Config\IssueDocuments::getInstance();

			// CHECK "docNumberType": THE DOCUMENT HAS A SERIES ATTACHED //////////////////////////
			if ($docCfgObj->getDocNumberType($documentType)=='chooseSeries' or $docCfgObj->getDocNumberType($documentType)=='pivotSeries') {

				// IF THE CURRENT PIVOT IS NOT VOIDED AND SERIES WAS SET --------------------------
				if (!$this->pivotIsVoided and $idDocumentSeries and !$preview) {

					// documents that are configured as 'alwaysNew' will get a new number - check 'preview' also
					if (!$docCfgObj->get($documentType)['numberReuse']) {
						$this->newNumber=true;
					}

					// documents that have series attached but reuse the number
					if ($docCfgObj->get($documentType)['numberReuse']) {
						// search through the generated documents - to see if a document was already generated
						$found=false;

						foreach($pivotObj->documentsList as $docDet) {
							if ($docDet['docType']==$documentType and !$docDet['preview']) {
								$found=true; // if document number is found - newNumber remains false
								break;
							}
						}
						// if document number is not set - this is the first generation (check preview flag)
						if (!$found) {
							$this->newNumber=true;
						}
					}

				}

			}

			// CHECK "docNumberType": THE DOC NUMBER IS MANUALLY INPUTTED OR TAKEN FROM PIVOT /////
			elseif ($docCfgObj->getDocNumberType($documentType)=='manualNumber' or $docCfgObj->getDocNumberType($documentType)=='pivotNumber') {

				// if the current generation is not a preview
				if (!$preview) {

					// determine the document number into $docNumber
					$newDocNo = null;
					if ($docCfgObj->getDocNumberType($documentType)=='manualNumber') {
						$newDocNo=$this->fields['docNumber'];
					}
					if ($docCfgObj->getDocNumberType($documentType)=='pivotNumber') {
						if ($this->fields['pivot']=='invoice') $newDocNo=$pivotObj->invoiceNumber;
						if ($this->fields['pivot']=='cashing') $newDocNo=$pivotObj->docNumber;

						if ($this->fields['pivot'] == 'payment') {
							$newDocNo = $pivotObj->docNumber;
						}
						if ($this->fields['pivot'] == 'cashBookDoc') {
							$newDocNo = $pivotObj->number;
						}

					}

					// if some document number set - check if it is the first time it is used
					if ($newDocNo) {
						// search through the generated documents - to see if a document was already generated
						$found=false;
						foreach($pivotObj->documentsList as $docDet) {
							if ($docDet['docType']==$documentType and $docDet['docNumber']==$newDocNo and !$docDet['preview']) {
								$found=true;
								break;
							}
						}
						// if document number was not used on this povit (for this doc type) - this is a new number
						if (!$found) {
							$this->newNumber=true;
						}
					}

				} // doc is not a preview

			}

		}

		///////////////////////////////////////////////////////////////////////////////////////////
		// IF THE DOCUMENT IS MANUALLY UPLOADED ///////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['generation']=='manual') {
			// if some docNumber set
			if ($this->fields['docNumber'] and !$preview) {
				// search through the generated documents - to see if a document was already generated
				$found=false;
				foreach($pivotObj->documentsList as $docDet) {
					if ($docDet['docType']==$documentType and $docDet['docNumber'] and !$docDet['preview']) {
						$found=true;
						break;
					}
				}
				// if document number was not used on this pivot (for this doc type) - this is a new number
				if (!$found) {
					$this->newNumber=true;
				}
			}
		}

	}



	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_filterSaveData()
	 */
	protected function save_filterSaveData(Array $saveData)
	{
		// FOR 'insert' OPERATION /////////////////////////////////////////////////////////////////
		if ($this->operation=='insert') {

			// get all fields
			$saveData = [
				'idPivot'=>$this->fields['idPivot'],
				'pivot'=>$this->fields['pivot'],
				'docType'=>$this->fields['docType'],
				'preview'=>(Int)$this->fields['preview'],
				'generation'=>$this->fields['generation'],
				'emailSent'=>0,
				'disableEmailSending'=>(int)$this->fields['disableEmailSending'],
				'comments'=>(String)$this->fields['comments'],
				'extraInfo'=>($this->fields['extraInfo']?:[])
			];

			// attach the document template
			if ($this->fields['idTemplate']) {
				$saveData['idTemplate']=$this->fields['idTemplate'];
			}

			// sets the fieldsDetails key in $saveData
			$fieldsDetails=$this->save_filterSaveData_setFieldsDetails();
			if ($fieldsDetails) {
				$saveData['fieldsDetails'] = serialize($fieldsDetails);
			} else {
				$saveData['fieldsDetails'] = '';
			}

			// sets the documentNumber value
			$this->save_filterSaveData_setDocumentNumber();


			// set the document format
			if ($this->fields['generation']=='auto') {
				$saveData['format'] = \Entity\DocumentTemplate::select($this->fields['idTemplate'])->format;
			} else {
				$saveData['format']=$this->fields['format'];
			}

			// attach the document date
			$saveData['docDate']=$this->documentDate;

			// attach the document date
			$saveData['docNumber'] = ($this->documentNumber?:0); // it may be null, force it to 0 when not set

			// stores the history flag to $saveData.
			$saveData['history'] = serialize( [ ['op'=>'g', 'us'=>(int)$this->fields['idIssueUser'], 't'=>TIMESTAMP] ] );

			// add extra info
			$ei = [];
			// add doc series
			if ($this->fields['idDocumentSeries']) {
				$ei['idDocumentSeries']=$this->fields['idDocumentSeries'];
			}
			// add auto process rule
			if ($this->fields['idAutoProcessRule']) {
				$ei['idAutoProcessRule']=$this->fields['idAutoProcessRule'];
			}
			// pivot is voided?
			if ($this->pivotIsVoided) {
				$ei['voided']=1;
			}
			// add the servicesList element
			if ($this->fields['servicesList']) {
				$ei['servicesList']=array_values($this->fields['servicesList']);
			}

			// serialize the field
			$extraInfo = array_merge((array)$saveData['extraInfo'], $ei);

			if ($extraInfo) {
				$saveData['extraInfo'] = serialize($extraInfo);
			}
			else {
				$saveData['extraInfo'] = '';
			}

			// rewrites some pivot fields valus
			$this->save_filterSaveData_setAutoFieldsValues();

			// the doc file name
			$saveData['docFile'] = $this->save_filterSaveData_getDocumentFile();

		}

		// FOR 'update' OPERATION /////////////////////////////////////////////////////////////////
		if ($this->operation=='update') {
			if ($saveData['extraInfo']) {
				$extraInfo = \Entity\Document::select($this->getId())->extraInfo;
				foreach ($saveData['extraInfo'] as $key => $value) {
					if (is_null($value)) {
						unset ($extraInfo[$key]);
					}
					else {
						$extraInfo[$key] = $value;
					}
				}

				// store the new extra info
				if ($extraInfo) {
					$saveData['extraInfo'] = serialize($extraInfo);
				}
				else {
					$saveData['extraInfo'] = '';
				}
			}

			unset($saveData['docFileContent']);
		}

		// FOR 'sendMail' OPERATION //////////////////////////////////////////////////////////////
		if ($this->operation=='sendMail') {
			unset($saveData['forceSenderEmail'], $saveData['forceSenderName']);
		}

		// FOR 'openDocument' OPERATION //////////////////////////////////////////////////////////
		if ($this->operation == 'openDocument') {
			unset ($saveData['action']);
		}

		return $saveData;

	}



	/**
	 * Sets the fieldsDetails key in saveData.
	 * @return Array - the default field details
	 */
	private function save_filterSaveData_setFieldsDetails()
	{

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}

		$fieldsDetails = [];
		if ($this->fields['idTemplate']) {
			$docTpl=\Entity\DocumentTemplate::select($this->fields['idTemplate']);
		} else {
			return $fieldsDetails;
		}

		// add standard fields
		if ($docTpl->fieldsDetails and $this->fields['standardFieldsValues']) {
			foreach($docTpl->fieldsDetails as $fld=>$type) {
				if ($type=='optional' or $type=='mandatory') {
					$fieldsDetails[$fld]= ['t'=>$type, 'f'=>'text', 'val'=>$this->fields['standardFieldsValues'][$fld]];
				}
			}
		}

		// add custom fields
		if ($docTpl->customFieldsDetails and $this->fields['customFieldsValues']) {
			foreach($docTpl->customFieldsDetails as $fld=>$fldDet) {
				if ($fldDet['type']=='optional' or $fldDet['type']=='mandatory') {
					// determine format
					if ($fldDet['inputType']=='selector') $format='selector';
					elseif ($fldDet['inputType']=='textarea') $format='test';
					else $format='string';
					// add field
					$fieldsDetails[$fld]= ['t'=>$fldDet['type'], 'f'=>$format, 'val'=>$this->fields['customFieldsValues'][$fld], 'n'=>$fldDet['name']];
					if ($fldDet['selectorValues']) $fieldsDetails[$fld]['opt']=$fldDet['selectorValues'];
				}
			}
		}

		// return the fieldsDetails
		return $fieldsDetails;

	}

	/**
	 * Sets the internal documentNumber attribute value.
	 */
	private function save_filterSaveData_setDocumentNumber()
	{

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}

		// INITIALIZATIONS ////////////////////////////////////////////////////////////////////////

		// assume that documentNumber is 0 - will be later rewritten
		$this->documentNumber=0;

		// these values are stored as simple vars as it is easier to work with them like this
		// the current document type
		$documentType=$this->fields['docType'];
		// the current document series id
		$idDocumentSeries=$this->fields['idDocumentSeries'];
		// preview flag
		$preview=$this->fields['preview'];
		// the current pivot object
		$pivotObj=$this->getInsertingDocumentPivot();

		///////////////////////////////////////////////////////////////////////////////////////////
		// IF THE DOCUMENT IS AUTO-GENERATED //////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['generation']=='auto') {

			$docCfgObj = \App\Config\IssueDocuments::getInstance();

			// CHECK "docNumberType": THE DOCUMENT HAS A SERIES ATTACHED //////////////////////////
			if ($docCfgObj->getDocNumberType($documentType)=='chooseSeries' or $docCfgObj->getDocNumberType($documentType)=='pivotSeries') {

				// IF THE CURRENT PIVOT IS NOT VOIDED AND SERIES IS ASSOCIATED --------------------
				if (!$this->pivotIsVoided and $idDocumentSeries) {

					// documents that are configured as 'alwaysNew' will get a new number - check 'preview' also
					if (!$docCfgObj->get($documentType)['numberReuse']) {

						if (!$preview) {
							$this->documentNumber = \Entity\DocumentSeries::selectForUpdate($idDocumentSeries)->nextNumber;

							// increase next number
							$documentSeriesObj = \Entity\DocumentSeries::update($idDocumentSeries);
							$documentSeriesObj->skipUserAccessOnOperations = true;
							$documentSeriesObj->increaseNextNumber = true;
							$documentSeriesObj->lastDate = $this->documentDate;
							$documentSeriesObj->save();

						} else {
							$this->documentNumber=\Entity\DocumentSeries::selectForUpdate($idDocumentSeries)->nextNumber;
						}
					}
					// documents that have series attached but reuse the number
					else {

						// search through the generated documents - to see if a document was already generated
						foreach($pivotObj->documentsList as $docDet) {
							if ($docDet['docType']==$documentType and !$docDet['preview']) {
								$this->documentNumber=$docDet['docNumber']; // if document number is found use it
								break;
							}
						}
						// if document number is not set - this is the first generation (check preview flag)
						if (!$this->documentNumber) {
							if (!$preview) {
								$this->documentNumber = \Entity\DocumentSeries::selectForUpdate($idDocumentSeries)->nextNumber;

								// increase next number
								$documentSeriesObj = \Entity\DocumentSeries::update($idDocumentSeries);
								$documentSeriesObj->skipUserAccessOnOperations = true;
								$documentSeriesObj->increaseNextNumber = true;
								$documentSeriesObj->lastDate = $this->documentDate;
								$documentSeriesObj->save();

							} else {
								$this->documentNumber=\Entity\DocumentSeries::selectForUpdate($idDocumentSeries)->nextNumber;
							}
						}
					}

				}
				// IF THE CURRENT PIVOT IS VOIDED -------------------------------------------------
				else {
					// sets the document number as the last generated document (of the current type) on this pivot
					// search through the generated documents - to see if a document was already generated
					foreach($pivotObj->documentsList as $docDet) {
						if ($docDet['docType']==$documentType and $docDet['docNumber']) {
							$this->documentNumber=$docDet['docNumber']; // does not break so the last number is used
						}
					}
				}

			}

			// CHECK "docNumberType": THE DOC NUMBER IS MANUALLY INPUTTED OR TAKEN FROM PIVOT /////
			elseif ($docCfgObj->getDocNumberType($documentType)=='manualNumber' or $docCfgObj->getDocNumberType($documentType)=='pivotNumber') {

				// determine the document number into $docNumber
				if ($docCfgObj->getDocNumberType($documentType)=='manualNumber') {
					$this->documentNumber=$this->fields['docNumber'];
				}
				if ($docCfgObj->getDocNumberType($documentType)=='pivotNumber') {
					switch ($this->fields['pivot']) {
						case 'invoice':
							$this->documentNumber=$pivotObj->invoiceNumber;
						break;
						case 'cashing': case 'payment': case 'importedDocument':
							$this->documentNumber=$pivotObj->docNumber;
						break;
						case 'cashBookDoc':
							$this->documentNumber = $pivotObj->number;
						break;
						case 'balance':
							$this->documentNumber = $pivotObj->balanceNumber;
						break;
					}
				}

			}
			// log any type of invalid doc number
			else {
				throw new \Exception('This type of doc number is not valid: '.$docCfgObj->getDocNumberType($documentType));
			}

		}

		///////////////////////////////////////////////////////////////////////////////////////////
		// IF THE DOCUMENT IS MANUALLY UPLOADED ///////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['generation']=='manual') {
			// the manually inputted number is the document number
			if ($this->fields['docNumber']) {
				$this->documentNumber=$this->fields['docNumber'];
			}
		}

	}

	/**
	 * Manually set some pivot fields values.
	 */
	private function save_filterSaveData_setAutoFieldsValues()
	{

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}


		// skip this step if the document is 'manual'
		if ($this->fields['generation']=='manual') return ;

		// simplified id pivot var
		$id=$this->fields['idPivot'];

		// the current pivot object
		$pivotObj=$this->getInsertingDocumentPivot();

		// MANUALLY SET THE documentNumber, documentSeries AND documentSeriesCode /////////////////
		if ($this->fields['idDocumentSeries']) {
			\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue('documentSeriesId', $this->fields['idDocumentSeries']);
			\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue('documentSeriesCode', \Entity\DocumentSeries::selectForUpdate($this->fields['idDocumentSeries'])->code);
		}
		if ($this->documentNumber) {
			$docNoFormatted=$this->documentNumber;
			if ($this->fields['idDocumentSeries']) {
				$thisDocSer = \Entity\DocumentSeries::selectForUpdate($this->fields['idDocumentSeries']);
				$docNoFormatted=str_pad($docNoFormatted, $thisDocSer->length, "0", STR_PAD_LEFT);
			}
			\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->setManualValue('documentNumber', $docNoFormatted);
		}

		// SET ISSUE "INVOICE" FIELDS /////////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='invoice' and $this->newNumber and !$this->pivotIsVoided and !$pivotObj->issueTime) {
			if (isset($this->fields['idIssueUser'])) {
				$user=User::select($this->fields['idIssueUser']);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserName', $user->name);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserLastName', $user->lastName);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserFirstName', $user->firstName);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserIdSeries', $user->ID_series);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserIdNumber', $user->ID_number);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserIdReleaseBy', $user->ID_releaseBy);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserPNC', $user->PNC);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserJob', $user->job);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserEmail', $user->email);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserTelephone', $user->telephone);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserDeptName', $user->department->name);
				\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueUserDeptCode', $user->department->code);
			}
			\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invIssueTimestamp', date(DATETIME_FORMAT, TIMESTAMP));
			\App\GenDocument\Vars\Values_Invoice::getInstance($id)->setManualValue('invNumber', $this->documentNumber);
		}

		// SET ISSUE "CASHING" FIELDS /////////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='cashing' and $this->newNumber and !$this->pivotIsVoided and !$pivotObj->issueTime) {
			if (isset($this->fields['idIssueUser'])) {
				$user=User::select($this->fields['idIssueUser']);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserName', $user->name);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserLastName', $user->lastName);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserFirstName', $user->firstName);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserIdSeries', $user->ID_series);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserIdNumber', $user->ID_number);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserIdReleaseBy', $user->ID_releaseBy);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserPNC', $user->PNC);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserJob', $user->job);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserEmail', $user->email);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserTelephone', $user->telephone);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserDeptName', $user->department->name);
				\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueUserDeptCode', $user->department->code);
			}
			\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashIssueTimestamp', date(DATETIME_FORMAT, TIMESTAMP));
			\App\GenDocument\Vars\Values_Cashing::getInstance($id)->setManualValue('cashNumber', $this->documentNumber);
		}

		// SET ISSUE "PAYMENT" FIELDS /////////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='payment' and $this->newNumber and !$this->pivotIsVoided and !$pivotObj->issueTime) {
			if (isset($this->fields['idIssueUser'])) {
				$user=User::select($this->fields['idIssueUser']);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserName', $user->name);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserLastName', $user->lastName);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserFirstName', $user->firstName);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserIdSeries', $user->ID_series);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserIdNumber', $user->ID_number);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserIdReleaseBy', $user->ID_releaseBy);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserPNC', $user->PNC);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserJob', $user->job);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserEmail', $user->email);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserTelephone', $user->telephone);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserDeptName', $user->department->name);
				\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentIssueUserDeptCode', $user->department->code);
			}
			\App\GenDocument\Vars\Values_Payment::getInstance($id)->setManualValue('paymentNumber', $this->documentNumber);
		}

		// SET ISSUE "CASHBOOK DOC" FIELDS ////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='cashBookDoc' and $this->newNumber and !$this->pivotIsVoided) {
			if (isset($this->fields['idIssueUser'])) {
				$user=User::select($this->fields['idIssueUser']);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserName', $user->name);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserLastName', $user->lastName);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserFirstName', $user->firstName);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserIdSeries', $user->ID_series);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBooktIssueUserIdNumber', $user->ID_number);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserIdReleaseBy', $user->ID_releaseBy);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserIdReleaseDateIso', $user->ID_releaseDate);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserIdReleaseDateStandard', \Engine\DateTime::isoToStd($user->ID_releaseDate));
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserPNC', $user->PNC);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserJob', $user->job);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserEmail', $user->email);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserTelephone', $user->telephone);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserDeptName', $user->department->name);
				\App\GenDocument\Vars\Values_CashBookDoc::getInstance($id)->setManualValue('cashBookIssueUserDeptCode', $user->department->code);
			}
		}

		// SET ISSUE "IMPORTED DOC" FIELDS ////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='importedDocument' and $this->newNumber and !$this->pivotIsVoided) {
			\App\GenDocument\Vars\Values_ImportedDocument::getInstance($id)->setManualValue('docNumber', $this->documentNumber);
		}

	}

	/**
	 * Returns the docFile attribute depending on the type of generation.
	 */
	private function save_filterSaveData_getDocumentFile() : string
	{
		$time = time();	// do NOT use the TIMESTAMP constant, because we can generate 2 documents in the same second

		// checks operation
		if ($this->operation!='insert') {
			throw new \Exception("You can call `".__METHOD__."` only for `insert` operations.");
		}

		// FOR 'manual' DOCUMENTS /////////////////////////////////////////////////////////////////
		$fileName = null;
		if ($this->fields['generation']=='manual') {
			// determine the final file name for the file - it is the fileName.TIMESTAMP.fileExtension
			$fileName=\Engine\File::getName($this->fields['docFile']).'['.$time.'].'.\Engine\File::getExtension($this->fields['docFile']);
		}

		// FOR 'auto' DOCUMENTS ///////////////////////////////////////////////////////////////////
		if ($this->fields['generation']=='auto') {

			// get the name from the template
			$docTpl=\Entity\DocumentTemplate::select($this->fields['idTemplate']);
			// determint file name from template details or generate a default one - the doc type
			$fileName=$docTpl->fileName;
			if (!$fileName) $fileName=$this->fields['docType'];
			$fileName.='['.$time.'].'.$docTpl->extension;

			// REPLACE VARS IN FILE NAME ----------------------------------------------------------
			if (strpos($fileName, '%')!==false) {
				// instantiate document[docType]InvoiceFields
				$documentFieldsValues=\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'], $this->fields['docType']);
				// replace variables in the file name
				$textProcessor = new \Engine\GenDocument\TextProcessor\TextProcessor(new \Engine\GenDocument\TextProcessor\DefaultTextHandler(), $documentFieldsValues);
				$textProcessor->reset();
				$fileName=$textProcessor->parse($fileName);
			}

		}

		// clean the file name and attach it to the class object
		return preg_replace('/[^\p{L}0-9\.\-\_\[\]\ ]/u', '', htmlUnclean($fileName));

	}


	/**
	 * Skips the idAddUser and similar fields.
	 * @see \App\Entity\GeneralAbstract::save_finalSaveData()
	 */
	protected function save_finalSaveData(Array $saveData)
	{
		return $saveData;
	}



	/**
	 * Treats the sendMail operation (the other ones are processed by the parent method).
	 * @see \App\Entity\GeneralAbstract::save_process()
	 */
	protected function save_process(Array $saveData)
	{
		if ($this->operation=='sendMail') {
			$this->save_executeSendMail($saveData);
		}
		elseif ($this->operation == 'openDocument') {
			$this->save_executeOpenDocument($saveData);
		}
		elseif ($this->operation == 'deleteFile') {
			$this->save_executeDeleteFile($saveData);
		}
		else {
			parent::save_process($saveData);
		}
	}

	/**
	 * Does the actual send of email.
	 * @param Array $saveData
	 */
	private function save_executeSendMail(Array $saveData)
	{
		// PREPARE THE FIELDS FOR THE SENDING OF THE EMAIL ////////////////////////////////////////

		// the uncleaned mail body
		$emailBody=htmlUnclean($saveData['body']);

		// from email/name
		$idSenderUser = $this->fields['idSenderUser']; // by default it's the sending user but it's reset if forced as a manual email address
		$senderUser = User::select($this->fields['idSenderUser']);
		$fromEmail = $senderUser->email;
		$fromName = $senderUser->name;
		if ($this->fields['forceSenderEmail']) {
			$fromEmail = $this->fields['forceSenderEmail'];
			$idSenderUser = 0;
		}
		if (array_key_exists('forceSenderName', $this->fields)) {
			$fromName = $this->fields['forceSenderName'];
		}

		foreach ($saveData['generatedFiles'] as $idFile) {

			$documentEntitySelect = self::select($idFile);
			if (in_array($documentEntitySelect->pivotCode, ['invoice', 'cashing'])) {
				$forceEmailCfg = \App\Config\AutoEmailSender::getInstance()->get($documentEntitySelect->pivotCode);
				if ($forceEmailCfg['email']) {
					$fromEmail = $forceEmailCfg['email'];
					$fromName = $forceEmailCfg['name'];
					$idSenderUser = 0;
					break;
				}
			}
		}


		// collect generated and uploaded documents into these arrays
		$genDocs = [];
		$uplDocs = [];

		// load cfg
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

		// SEND THE EMAIL /////////////////////////////////////////////////////////////////////////
		$calendarFile = null;
		try {
			// START MAIN SETTINGS FOR EMAIL SENDING ----------------------------------------------
			$email = \Entity\Email::insert();
			$email->subject = $saveData['subject'];
			$email->body = $emailBody;
			$email->fromEmail = $fromEmail;
			if ($fromName) {
				$email->fromName = $fromName;
			}
			$email->idSenderUser = $idSenderUser;

			// ADDS "TO" AND "CC" RECEIVERS -------------------------------------------------------
			$toList = [];
			if ($saveData['receiversTo']) {
				foreach($saveData['receiversTo'] as $addr) {
					$toList[] = $addr;
				}
			}
			$email->to = $toList;
			
			$ccList = [];
			if ($saveData['receiversCc']) {
				foreach($saveData['receiversCc'] as $addr) {
					$ccList[] = $addr;
				}
			}
			$email->cc = $ccList;

			// ADDS BCC AND READ-RECEIPT ----------------------------------------------------------
			$bccList = [];
			if ($saveData['selfBcc']) {
				$bccList[] = $fromEmail;
			}
			$email->bcc = $bccList;

			if ($saveData['sendReadReceipt']){
				$email->readReceipt = 1;
			}

			// ADD DOCUMENTS ATTACHMENTS ----------------------------------------------------------
			$attachments = [];
			
			// Add document attachments
			if ($saveData['generatedFiles']) {
				foreach($saveData['generatedFiles'] as $idDoc) {
					$doc = self::select($idDoc);
					$attachments[] = [
						'name' => $doc->docFileClean,
						'fileNameWithPath' => $doc->docFilePath.$doc->docFile
					];
					$genDocs[$idDoc] = $doc->docFileClean;
				}
			}


			// ADD UPLOADED FILES -----------------------------------------------------------------
			if ($saveData['uploadFiles']) {

				// the path of the file
				$path = APP_REAL_PATH.$CFG['paths']['uploadedDocumentsEmail'];

				// create path if not exists
				if (!\Engine\Dir::check($path)) {
					\Engine\Dir::create($path);
				}

				foreach ($saveData['uploadFiles'] as $fileName => $fileWithPath) {
					// upload file on disk
					$uniqueIdFileName = uniqid().mt_rand(2, 5).'.'.\Engine\File::getExtension($fileName);


					if (!\Engine\File::create($path.$uniqueIdFileName, \Engine\File::getContents($fileWithPath))) {
						$this->messages->addErrorCode('sendEmail_saveUploadedFile');
					}

					// attach file to mail
					$attachments[] = [
						'name' => $fileName,
						'fileNameWithPath' => $CFG['paths']['uploadedDocumentsEmail'].$uniqueIdFileName
					];

					// save history of files
					$uplDocs[$uniqueIdFileName] = $fileName;
				}

			}

			// ADD CALENDAR FILE ------------------------------------------------------------------
			if ($saveData['attachmentCalendar']) {

				// create and populate the calendar event
				$calendarObj = new \App\Utils\Calendar\Calendar();
				$docEnt = self::selectForUpdate($this->getId());

				$calendarEventsCreate = [];
				// this is for service entity
				if ($docEnt->pivotCode=='service') {
					$calendarEventsCreate[] = new \App\GenDocument\PivotCalendarEvent\Service($calendarObj, $docEnt->pivot);
				}
				if ($docEnt->pivotCode=='order') {
					foreach (array_keys($docEnt->pivot->servicesList) as $idService) {
						$calendarEventsCreate[] = new \App\GenDocument\PivotCalendarEvent\Service($calendarObj, \Entity\Service::select($idService));
					}
				}

				foreach ($calendarEventsCreate as $calendarEventCreate) {
					$calendarEventCreate->populate();
				}

				if ($calendarObj->getEventsList()) {
					// get calendar output
					$calendarWriterObj = new \App\Utils\Calendar\ICal\Writer($calendarObj);
					$calendarFileContent = $calendarWriterObj->generate();

					$attachments[] = [
						'name' => 'calendar.ics',
						'fileContent' => $calendarFileContent
					];
				}
			}

			// Set attachments
			$email->attachments = $attachments;

			$email->category = 'Document';
			$email->priority = 3;

			// Save and send email
			$email->save();
			
		} catch (\App\Entity\Exception $e) {
			\Log::getInstance()->error("Could not send document(s) via email due to a technical problem. Message: ".$e->getMessagesCollector()->getMessageBulk());
			$this->messages->addMessageCollector($e->getMessagesCollector());
			$this->messages->throwMessages();
		} finally {
			// eliminate temp file for calendar event
			if ($calendarFile) {
				\Engine\File::delete($calendarFile);
			}
		}


		// UPDATE THE EMAIL SENDING OPERATION IN DOCUMENT HISTORY /////////////////////////////////

		// compose the new history element
		$historyItem = [
			'op'=>'e',
			'us'=>$this->fields['idSenderUser'],
			't'=>TIMESTAMP,
			'details' => [
				'to' =>				$saveData['receiversTo'],
				'cc' =>				$saveData['receiversCc'],
				'bcc' =>			$saveData['selfBcc'],
				'readReceipt' =>	$saveData['sendReadReceipt'],
				'genDocs' =>		$genDocs,
				'uplDocs' =>		$uplDocs,
				'body' =>			$emailBody,
				'subject' =>		$saveData['subject'],
				'attachmentCalendar' => (Bool)$saveData['attachmentCalendar'],
			]
		];

		// determine the new history array for this document
		$docFullHistory=self::selectForUpdate($this->id)->history;
		$docFullHistory[]=$historyItem;

		// update the history for this document
		$this->save_executeSql_update($this->tableName, ['history'=>serialize($docFullHistory), 'emailSent'=>1], 'id', $this->id);

		// send was successful - display a success message
		$this->messages->addSuccessCode('sendMail_ok');

	}

	private function save_executeOpenDocument(Array $saveData)
	{

		// compose history element
		$historyItem = [];

		switch ($this->fields['action']) {
			case 'download':
				$historyItem = [
					'op' => 'd',
					'us' => CURRENT_ID_USER,
					't' => 	TIMESTAMP,
				];
			break;

			case 'view':
				$historyItem = [
					'op' => 'v',
					'us' => CURRENT_ID_USER,
					't' => 	TIMESTAMP,
				];
			break;

			case 'print':
				$historyItem = [
					'op' => 'p',
					'us' => CURRENT_ID_USER,
					't' => 	TIMESTAMP,
				];
			break;

			default:
				throw new \Exception("Wrong action ({$this->fields['action']}) for the '".__CLASS__."' class, operation=`{$this->operation}`.");
		}


		// determine the new history array for this document
		$docFullHistory = self::selectForUpdate($this->id)->history;
		$docFullHistory[]=$historyItem;

		// update the history for this document
		$this->save_executeSql_update($this->tableName, ['history'=>serialize($docFullHistory)], 'id', $this->id);

		// send was successful - display a success message
		$this->messages->addSuccessCode('openDocument_ok');
	}

	/**
	 * Deletes the document file and removes it from the db
	 * @param array $saveData
	 */
	private function save_executeDeleteFile(Array $saveData)
	{
		$docEnt = self::selectForUpdate($this->id);

		$historyItem = [
			'op' => 'r',
			'us' => CURRENT_ID_USER,
			't' => 	TIMESTAMP,
		];

		// determine the new history array for this document
		$docFullHistory = $docEnt->history;
		$docFullHistory[]=$historyItem;

		// update the history for this document
		$this->save_executeSql_update($this->tableName, ['docFile'=>'', 'history'=>serialize($docFullHistory)], 'id', $this->id);

		if (!\Engine\File::delete($docEnt->docFileNameWithPath)) {
			$this->messages->removeSuccess();
			$this->messages->addErrorCode('deleteFile_fileNotDeleted');
		}
	}

	/**
	 * For 'insert' operation - some extra stuff is done: the pivot is updated and the document is
	 * written on disk.
	 * @see \App\Entity\GeneralAbstract::save_postProcess()
	 */
	protected function save_postProcess(Array $saveData)
	{

		// INSERT OPERATION ///////////////////////////////////////////////////////////////////////
		if ($this->operation=='insert') {

			// update pivot field - generatedDocuments and other fields
			$this->save_postProcess_updatePivot();

			// write the document on disk
			$this->save_postProcess_createDocumentFile($saveData);

			// eliminate the manually set document pivot fields
			\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'])->flushManualValues();

		}

		// UPDATE OPERATION ///////////////////////////////////////////////////////////////////////
		if ($this->operation=='update') {

			$this->save_postProcess_rewriteDocContent();

		}

	}

	/**
	 * Updates the pivot field 'generatedDocuments' and other fields like invoiceNumber/docNumber, issueTime, issueUser - for invoice/cashing.
	 * It also updates the corporate debit information.
	 */
	private function save_postProcess_updatePivot()
	{

		// if document number is not new or is not set or it is a preview or the pivot is voided - skip this step.
		if (!$this->newNumber or !$this->documentNumber or $this->fields['preview'] or $this->pivotIsVoided) return ;

		// the current pivot reference
		$pivotObj=$this->getInsertingDocumentPivot();

		// determine the table which will be updated
		global $DBTReservations, $DBTReservationServices, $DBTReservationServiceInvoices, $DBTReservationServiceInvoiceCashing, $DBTSupplierInvoicePayment, $DBTInvoiceBalances, $DBTCashBookDocuments, $DBTImportedOrders_services, $DBTImportedDocuments_documents;
		$pivotTableName = [
				'order' => 				$DBTReservations,
				'service' => 			$DBTReservationServices,
				'invoice' => 			$DBTReservationServiceInvoices,
				'cashing' => 			$DBTReservationServiceInvoiceCashing,
				'payment' => 			$DBTSupplierInvoicePayment,
				'balance' =>			$DBTInvoiceBalances,
				'cashBookDoc' => 		$DBTCashBookDocuments,
				'importedService' => 	$DBTImportedOrders_services,
				'importedDocument' => 	$DBTImportedDocuments_documents,
		];


		///////////////////////////////////////////////////////////////////////////////////////////
		// SAVE PIVOT FIELDS //////////////////////////////////////////////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		$fields = [];

		// CREATE SQL FIELDS //////////////////////////////////////////////////////////////////////
		if ($this->fields['pivot'] != 'cashBookDoc' and $this->fields['pivot'] != 'importedService' and $this->fields['pivot'] != 'payment' and $this->fields['pivot'] != 'importedDocument') {
			$fields['sql:generatedDocuments']="CONCAT(generatedDocuments, '{$this->fields['docType']}:{$this->documentNumber}|')";
		}

		// if pivot = invoice; also checks if the invoiceNumber and issue details should be updated
		if ($this->fields['pivot']=='invoice' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			$fields['invoiceNumber'] = $this->documentNumber;
			$fields['issueTime'] = TIMESTAMP;
			$fields['idIssueUser'] = (int)$this->fields['idIssueUser'];
		}
		// if pivot = cashing; also checks if the invoiceNumber and issue details should be updated
		if ($this->fields['pivot']=='cashing' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			$fields['docNumber'] = $this->documentNumber;
			$fields['issueTime'] = TIMESTAMP;
			$fields['idIssueUser'] = (int)$this->fields['idIssueUser'];
		}

		// if pivot = payment; also checks if the invoiceNumber and issue details should be updated
		if ($this->fields['pivot']=='payment' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			$fields['docNumber'] = $this->documentNumber;
			$fields['issueTime'] = TIMESTAMP;
			$fields['idIssueUser'] = (int)$this->fields['idIssueUser'];
		}

		// if pivot = cashBookDoc;
		if ($this->fields['pivot']=='cashBookDoc' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			$fields['number'] = $this->documentNumber;
			$fields['issueTime'] = TIMESTAMP;
			$fields['idIssueUser'] = (int)$this->fields['idIssueUser'];
		}

		// if pivot = importedDocument;
		if ($this->fields['pivot']=='importedDocument' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType'])) {
			$fields['docNumber'] = $this->documentNumber;
			$fields['issueTime'] = DATETIME;
			$fields['idIssueUser'] = (int)$this->fields['idIssueUser'];
		}

		// RUN AND CHECK SQL //////////////////////////////////////////////////////////////////////
		if ($this->fields['pivot'] == 'cashBookDoc' or $this->fields['pivot'] == 'importedService' or $this->fields['pivot'] == 'importedDocument') {
			$pivotTable = $pivotTableName[$this->fields['pivot']];
		}
		else {
			$pivotTable = $pivotTableName[$this->fields['pivot']].$this->getTableMonth();
		}

		if ($fields) {
			$this->save_executeSql_update($pivotTable, $fields, 'id', $this->fields['idPivot']);
		}

		///////////////////////////////////////////////////////////////////////////////////////////
		// UPDATE CLIENT BALANCE INFORMATION ON FIRST INVOICE ISSUANCE ////////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='invoice' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			if ($pivotObj->idCorporate) {
				\App\PartnerBalance\Corporate::getInstance($pivotObj->idCorporate)->updateBalance_issueInvoice($pivotObj->currency, $pivotObj->printTotal);
			}
			if ($pivotObj->idCustomer) {
				\App\PartnerBalance\Customer::getInstance($pivotObj->idCustomer)->updateBalance_issueInvoice($pivotObj->currency, $pivotObj->printTotal);
			}
		}

		///////////////////////////////////////////////////////////////////////////////////////////
		// UPDATE CORPORATE HISTORY BALANCE (delete the stale balances) ///////////////////////////
		///////////////////////////////////////////////////////////////////////////////////////////
		if ($this->fields['pivot']=='invoice' and \App\Config\IssueDocuments::getInstance()->isMainDocOnPivot($this->fields['docType']) and !$pivotObj->issueTime) {
			if ($pivotObj->idCorporate) {
				\App\PartnerBalance\History\Corporate::getInstance($pivotObj->idCorporate, $pivotObj->currency)->flushBalances($pivotObj->invoiceDate);
			}
			if ($pivotObj->idCustomer) {
				\App\PartnerBalance\History\Customer::getInstance($pivotObj->idCustomer, $pivotObj->currency)->flushBalances($pivotObj->invoiceDate);
			}
		}

	}

	/**
	 * Creates the document file and writes it to disk.
	 */
	private function save_postProcess_createDocumentFile(Array $saveData)
	{

		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

		// GET DOCUMENT CONTENT IN MEMORY /////////////////////////////////////////////////////////
		// IF generation = 'auto' -----------------------------------------------------------------
		$documentContent = null;
		if ($this->fields['generation']=='auto') {

			// document template object
			$docTpl=\Entity\DocumentTemplate::select($this->fields['idTemplate']);

			// determine the document generation count
			$pivotEntity=$this->getInsertingDocumentPivot();
			$docList=$pivotEntity->documentsList;
			// loop through each document (skip other doc type and previews)
			$docGenCount=1;
			foreach($docList as $doc) {
				if ($doc['docType']!=$this->fields['docType']) continue;
				if ($doc['preview']) continue;
				$docGenCount++;
			}

			// the pivot extra params
			$extraParams=[];
			if ($this->fields['servicesList']) {
				$extraParams['servicesList']=$this->fields['servicesList'];
			}

			// also associate the current document number that is given on the current issuance
			$extraParams['documentNumber'] = 	$this->documentNumber;
			$extraParams['idDocument'] = 		$this->getId();

			// doc field class
			$varProcessor=\App\GenDocument\Vars\PivotBuilderValues::fromPivot($this->fields['pivot'], $this->fields['idPivot'], $this->fields['docType']);
			$varProcessor->setExtraParams($extraParams);

			// GENERATE THE DOCUMENT ..............................................................
			try {
				$gen = new \App\GenDocument\Generator\General();
				$gen->loadTemplate($this->fields['idTemplate']);
				$gen->setPivot($this->fields['pivot'], $this->fields['idPivot']);
				$gen->markVoid($this->pivotIsVoided);
				$gen->markPreview($this->fields['preview']);
				$gen->setOutputFormat($docTpl->format);
				$gen->setVariableProcessor($varProcessor);
				$gen->setGenerationCount($docGenCount);
				$gen->setExtraParams($extraParams);
				// generate document content
				$documentContent = $gen->getOutput();
			} catch(\DTMOutputException $ex) {
				$this->messages->removeSuccess();
				$this->messages->addErrorText($ex->getMessage());
			} catch (\Engine\GenDocument\Generator\Exception $e) {
				$this->messages->removeSuccess();
				$this->messages->addErrorText($e->getMessage());
			}
		}
		// IF generation = 'manual' ---------------------------------------------------------------
		else {
			$documentContent=$this->fields['docContent'];
		}

		// add the possibility to do something else on this entity (after the operation has finished successfully)
		\Engine\Plugins\Manager::getInstance()->executeEvent('EntityExtension-[Document]-DocContentGenerated', [
			'entity'=>$this, 'documentContent'=>&$documentContent,
			'documentType'=>$this->fields['docType'], 'pivot'=>$this->fields['pivot'], 'idPivot'=>$this->fields['idPivot'],
			'idDocumentTemplate'=>$this->fields['idTemplate'], 'documentNumber'=>$this->documentNumber, 'servicesList'=>$this->fields['servicesList'],
			'customFieldsValues'=>$this->fields['customFieldsValues'],
			'saveData' => $saveData,
		]);

		// if no document content generated (either using DTM or using a plugin) - throw an error
		if (!$documentContent) {
			$this->messages->removeSuccess();
			$this->messages->addErrorCode('insert_fileContentInvalid');
		}

		// WRITE FILE ON DISK /////////////////////////////////////////////////////////////////////
		$fileWithFullPath=MODULE_REL_PATH.$CFG['paths']['generatedDocuments'].'/'.$this->getTableMonth().'/'.$this->fields['pivot'].'/'.$this->fields['idPivot'].'/'.$saveData['docFile'];
		if (is_file($fileWithFullPath)){
			throw new \Exception("File '{$fileWithFullPath}' already exists. We cannot overwrite it!");
		}

		// write file on disk
		if (!file_put_contents($fileWithFullPath, $documentContent)) {
			$this->messages->removeSuccess();
			$this->messages->addErrorCode('insert_fileNotWritten');
		}
		// adn change access mode
		if (!chmod($fileWithFullPath, 0777)) {
			$this->messages->removeSuccess();
			$this->messages->addErrorCode('insert_fileNotChmoded');
		}

	}


	/**
	 * Rewrites the content of the document if the
	 */
	private function save_postProcess_rewriteDocContent()
	{

		// if doc content must not be updated - skip this step
		if (!array_key_exists('docFileContent', $this->fields)) {
			return true;
		}

		// udpate file doc content
		$doc=Document::selectForUpdate($this->id);

		// rewrite file on disk and check the result
		if (!\Engine\File::create($doc->docFileNameWithPath, $this->fields['docFileContent'])) {
			$this->messages->addErrorCode('update_cantUpdateDocFileContent');
		}

		return true;

	}


	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_updateCache()
	 */
	protected function save_updateCache()
	{

		// CLEAN THE CACHE ////////////////////////////////////////////////////////////////////////
		if ($this->operation=='insert') {
			// CLEAN THE PIVOT CACHE --------------------------------------------------------------
			switch ($this->fields['pivot']) {
				case 'order':
					$this->save_updateCache_addEntry('Order', $this->fields['idPivot']);
					break;
				case 'service':
					$this->save_updateCache_addEntry('Service', $this->fields['idPivot']);// TODO: this should be 'OrderService' or 'Service'
					break;
				case 'invoice':
					$pivotObj=$this->getInsertingDocumentPivot();
					$this->save_updateCache_addEntry('Invoice', $this->fields['idPivot']);
					if ($pivotObj->idCorporate) {
						$this->save_updateCache_addEntry('Corporate', $pivotObj->idCorporate);
					}
					if ($pivotObj->idCustomer) {
						$this->save_updateCache_addEntry('Customer', $pivotObj->idCustomer);
					}
					break;
				case 'cashing':
					$this->save_updateCache_addEntry('InvoiceCashing', $this->fields['idPivot']);
					break;
				case 'payment':
					$this->save_updateCache_addEntry('SupplierInvoicePayment', $this->fields['idPivot']);
					break;

				case 'cashBookDoc' :
					$this->save_updateCache_addEntry('CashBookDocument', $this->fields['idPivot']);
					break;

				case 'importedService':
					$this->save_updateCache_addEntry('ImportedService', $this->fields['idPivot']);
					break;

				case 'importedDocument':
					$this->save_updateCache_addEntry('ImportedDocument', $this->fields['idPivot']);
					break;

				case 'balance':
					$this->save_updateCache_addEntry('Balance', $this->fields['idPivot']);
					break;

				default:
					throw new \Exception('Invalid pivot type when updating the document cache');
			}
		}

		// delete entity DocumentSeries cache
		\App\Entity\Cache::getInstance()->delete('DocumentSeries', $this->fields['idDocumentSeries']);

		// CLEAN THE CACHE FOR THE CURRENT ENTITY /////////////////////////////////////////////////
		parent::save_updateCache();

	}


	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_finishOperation()
	 */
	protected function save_finishOperation()
	{

		// EXPORT OF DOCUMENT IN XML FORMAT ///////////////////////////////////////////////////////
		if ($this->operation=='insert') {

			switch ($this->fields['pivot']) {
				case 'invoice':
					if ($this->newNumber) {
						try {
							$clientInvoiceExport = new \App\ExportData\ExportFile\ClientInvoice($this->fields['idPivot'], "issue", $this->fields['docType']);
							$clientInvoiceExport->setIdDocument($this->id);
							$clientInvoiceExport->writeFile();
						}
						catch (\App\MessagesException $ex) {
							$this->messages->removeSuccess();
							\Log::getInstance()->critical(sprintf('The invoice XML was not generated when issue the document. Error: %s', $ex->getMessagesCollector()->getMessageBulk()));
							$this->messages->addErrorCode('xmlExportFailed');
						}
					}
				break;

				case 'cashing':
					if ($this->newNumber) {
						try {
							$clientInvoiceCashingExport = new \App\ExportData\ExportFile\ClientInvoiceCashing($this->fields['idPivot'], "issue", $this->fields['docType']);
							$clientInvoiceCashingExport->setIdDocument($this->id);
							$clientInvoiceCashingExport->writeFile();
						}
						catch (\App\MessagesException $ex) {
							$this->messages->removeSuccess();
							\Log::getInstance()->critical(sprintf('The invoice cashing XML was not generated when issue the document. Error: %s', $ex->getMessagesCollector()->getMessageBulk()));
							$this->messages->addErrorCode('xmlExportFailed');
						}
					}
				break;

				case 'payment':
					if ($this->newNumber) {
						try {
							$supplierInvoicePaymentExport = new \App\ExportData\ExportFile\SupplierInvoicePayment($this->fields['idPivot'], "issue", $this->fields['docType']);
							$supplierInvoicePaymentExport->setIdDocument($this->id);
							$supplierInvoicePaymentExport->writeFile();
						}
						catch (\App\MessagesException $ex) {
							$this->messages->removeSuccess();
							\Log::getInstance()->critical(sprintf('The supplier invoice payment XML was not generated when issue the document. Error: %s', $ex->getMessagesCollector()->getMessageBulk()));
							$this->messages->addErrorCode('xmlExportFailed');
						}
					}
				break;

				case 'cashBookDoc':
					if ($this->newNumber) {
						try {
							$cashbookDocumentExport = new \App\ExportData\ExportFile\CashBookDocument($this->fields['idPivot'], 'issue', $this->fields['docType']);
							$cashbookDocumentExport->setIdDocument($this->id);
							$cashbookDocumentExport->writeFile();
						}
						catch (\App\MessagesException $ex) {
							$this->messages->removeSuccess();
							\Log::getInstance()->critical(sprintf('The cashbook document XML was not generated when issue the document. Error: %s', $ex->getMessagesCollector()->getMessageBulk()));
							$this->messages->addErrorCode('xmlExportFailed');
						}
					}
				break;

				case 'importedDocument':
					if ($this->newNumber) {
						try {
							$importedDocumentExport = new \App\ExportData\ExportFile\ImportedDocument($this->fields['idPivot'], 'issue', $this->fields['docType']);
							$importedDocumentExport->setIdDocument($this->id);
							$importedDocumentExport->writeFile();
						}
						catch (\App\MessagesException $ex) {
							$this->messages->removeSuccess();
							\Log::getInstance()->critical(sprintf('The imported document XML was not generated when issue the document. Error: %s', $ex->getMessagesCollector()->getMessageBulk()));
							$this->messages->addErrorCode('xmlExportFailed');
						}

					}
				break;

				default:
					// do nothing
			}
		}

		// ADD PLUGIN TRIGGER AFTER THE OPERATION FINISHED ////////////////////////////////////////
		if ($this->operation=='insert') { // TODO: sa nu se mai foloseasca asta ci cea din parinte
			\Engine\Plugins\Manager::getInstance()->executeEvent('EntityMtFinishOperation-[Document]', ['entity'=>$this, 'pivot'=>$this->fields['pivot'], 'idPivot'=>$this->fields['idPivot'], 'documentType'=>$this->fields['docType'], 'preview'=>$this->fields['preview'], 'newNumberForDocument'=>$this->newNumber]);
		}

		parent::save_finishOperation();

	}


	/**
	 * Returns the current state of 'pivotIsVoided' flag (look out for the correct operation).
	 * @return boolean
	 */
	public function getPivotIsVoidedFlag()
	{
		if ($this->operation!='insert') {
			throw new \Exception('The operation is not valid - it must be `insert` when using '.__METHOD__.'.');
		}
		return $this->pivotIsVoided;

	}

	/**
	 * Returns the new document number.
	 * @return number
	 */
	public function getDocumentNumber()
	{
		if ($this->operation!='insert') {
			throw new \Exception('The operation is not valid - it must be `insert` when using '.__METHOD__.'.');
		}
		return $this->documentNumber;
	}


	/**
	 * Creates month table for documents.
	 * @param string $date
	 * @throws \Exception
	 */
	static public function createMonthlyTables(string $date) :void
	{
		// CHECK IF INSIDE A TRANSACTION ----------------------------------------------------------
		if (\DB::isInsideTransaction()) {
			throw new \Exception("The monthly tables can only be created if NOT inside a transaction.");
		}

		// determint YM from date or YM format
		$tableMonth = \App\Utils\Db\TablesUtilsCached::getInstance()->determineYmFromString($date);

		// check table month
		if (!$tableMonth) {
			throw new \Exception('The date for document table is not valid in `'.__METHOD__.'`.');
		}

		$cfgObj = \Engine\Loader_Cfg::getInstance();

		// load table structure
		$cfgObj->loadCustomFile('dbStructure/operational.db.php');

		global $DBTDocuments;
		$CFG = $cfgObj->getGlobal();

		$autoIncrementStart = \App\Utils\Db\TablesUtils::getInstance()->getStartIndexFromYm($tableMonth);

		// create the documents table
		$createTableSQL = sprintf($CFG['monthTablesStructure']['documents'], $DBTDocuments.$tableMonth, $autoIncrementStart);
		\App\Utils\Db\TablesUtilsCached::getInstance()->createMonthTable($DBTDocuments, $tableMonth, $createTableSQL);

	}

}
