diff --git a/htdocs/accountancy/bookkeeping/list.php b/htdocs/accountancy/bookkeeping/list.php
index f0e95c3efb7..2d48bc706ea 100644
--- a/htdocs/accountancy/bookkeeping/list.php
+++ b/htdocs/accountancy/bookkeeping/list.php
@@ -715,58 +715,62 @@ if ($action == 'export_fileconfirm' && $user->hasRight('accounting', 'mouvements
}
}
- $mimetype = $accountancyexport->getMimeType($formatexportset);
-
- top_httphead($mimetype, 1);
-
- // Output data on screen
- $accountancyexport->export($object->lines, $formatexportset);
-
$notifiedexportdate = GETPOST('notifiedexportdate', 'alpha');
$notifiedvalidationdate = GETPOST('notifiedvalidationdate', 'alpha');
+ $withAttachment = !empty(trim(GETPOST('notifiedexportfull', 'alphanohtml'))) ? 1 : 0;
- if (!empty($accountancyexport->errors)) {
- dol_print_error('', '', $accountancyexport->errors);
- } elseif (!empty($notifiedexportdate) || !empty($notifiedvalidationdate)) {
- // Specify as export : update field date_export or date_validated
- $error = 0;
- $db->begin();
+ // Output data on screen or download
+ $result = $accountancyexport->export($object->lines, $formatexportset, $withAttachment);
- if (is_array($object->lines)) {
- foreach ($object->lines as $movement) {
- $now = dol_now();
+ $error = 0;
+ if ($result < 0) {
+ $error++;
+ } else {
+ if (!empty($notifiedexportdate) || !empty($notifiedvalidationdate)) {
+ if (is_array($object->lines)) {
+ // Specify as export : update field date_export or date_validated
+ $db->begin();
- $sql = " UPDATE ".MAIN_DB_PREFIX."accounting_bookkeeping";
- $sql .= " SET";
- if (!empty($notifiedexportdate) && !empty($notifiedvalidationdate)) {
- $sql .= " date_export = '".$db->idate($now)."'";
- $sql .= ", date_validated = '".$db->idate($now)."'";
- } elseif (!empty($notifiedexportdate)) {
- $sql .= " date_export = '".$db->idate($now)."'";
- } elseif (!empty($notifiedvalidationdate)) {
- $sql .= " date_validated = '".$db->idate($now)."'";
+ foreach ($object->lines as $movement) {
+ $now = dol_now();
+
+ $sql = " UPDATE ".MAIN_DB_PREFIX."accounting_bookkeeping";
+ $sql .= " SET";
+ if (!empty($notifiedexportdate) && !empty($notifiedvalidationdate)) {
+ $sql .= " date_export = '".$db->idate($now)."'";
+ $sql .= ", date_validated = '".$db->idate($now)."'";
+ } elseif (!empty($notifiedexportdate)) {
+ $sql .= " date_export = '".$db->idate($now)."'";
+ } elseif (!empty($notifiedvalidationdate)) {
+ $sql .= " date_validated = '".$db->idate($now)."'";
+ }
+ $sql .= " WHERE rowid = ".((int) $movement->id);
+
+ dol_syslog("/accountancy/bookkeeping/list.php Function export_file Specify movements as exported", LOG_DEBUG);
+
+ $result = $db->query($sql);
+ if (!$result) {
+ $error++;
+ break;
+ }
}
- $sql .= " WHERE rowid = ".((int) $movement->id);
- dol_syslog("/accountancy/bookkeeping/list.php Function export_file Specify movements as exported", LOG_DEBUG);
-
- $result = $db->query($sql);
- if (!$result) {
+ if (!$error) {
+ $db->commit();
+ } else {
$error++;
- break;
+ $accountancyexport->errors[] = $langs->trans('NotAllExportedMovementsCouldBeRecordedAsExportedOrValidated');
+ $db->rollback();
}
}
}
-
- if (!$error) {
- $db->commit();
- } else {
- $error++;
- $db->rollback();
- dol_print_error('', $langs->trans("NotAllExportedMovementsCouldBeRecordedAsExportedOrValidated"));
- }
}
- exit;
+
+ if ($error) {
+ setEventMessages('', $accountancyexport->errors, 'errors');
+ header('Location: '.$_SERVER['PHP_SELF']);
+ }
+ exit(); // download or show errors
}
}
@@ -854,7 +858,17 @@ if ($action == 'export_file') {
$form_question['separator3'] = array('name'=>'separator3', 'type'=>'separator');
}
- $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?'.$param, $langs->trans("ExportFilteredList").' ('.$listofformat[$formatexportset].')', $langs->trans('ConfirmExportFile'), 'export_fileconfirm', $form_question, '', 1, 350, 600);
+ // add documents in an archive for accountancy export (Quadratus)
+ if(getDolGlobalString('ACCOUNTING_EXPORT_MODELCSV') == AccountancyExport::$EXPORT_TYPE_QUADRATUS) {
+ $form_question['notifiedexportfull'] = array(
+ 'name' => 'notifiedexportfull',
+ 'type' => 'checkbox',
+ 'label' => $langs->trans('NotifiedExportFull'),
+ 'value' => 'false',
+ );
+ }
+
+ $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"].'?'.$param, $langs->trans("ExportFilteredList").' ('.$listofformat[$formatexportset].')', $langs->trans('ConfirmExportFile'), 'export_fileconfirm', $form_question, '', 1, 400, 600);
}
//if ($action == 'delbookkeepingyear') {
diff --git a/htdocs/accountancy/class/accountancyexport.class.php b/htdocs/accountancy/class/accountancyexport.class.php
index 3c45315ffa9..76b6782c174 100644
--- a/htdocs/accountancy/class/accountancyexport.class.php
+++ b/htdocs/accountancy/class/accountancyexport.class.php
@@ -36,6 +36,7 @@
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php';
+require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
/**
@@ -313,9 +314,10 @@ class AccountancyExport
*
* @param array $TData Array with data
* @param int $formatexportset Id of export format
- * @return void
+ * @param int $withAttachment [=0] Not add files or 1 to have attached in an archive (ex : Quadratus)
+ * @return int <0 if KO, >0 OK
*/
- public function export(&$TData, $formatexportset)
+ public function export(&$TData, $formatexportset, $withAttachment = 0)
{
global $conf, $langs;
global $search_date_end; // Used into /accountancy/tpl/export_journal.tpl.php
@@ -325,8 +327,44 @@ class AccountancyExport
$type_export = 'general_ledger';
global $db; // The tpl file use $db
+ $completefilename = '';
+ $exportFile = null;
+ $exportFileName = '';
+ $exportFilePath = '';
+ $archiveFileList = array();
+ if ($withAttachment == 1) {
+ // PHP ZIP extension must be enabled
+ if (!extension_loaded('zip')) {
+ $langs->load('install');
+ $this->errors[] = $langs->trans('ErrorPHPDoesNotSupport', 'ZIP');;
+ return -1;
+ }
+ } else {
+ $mimetype = $this->getMimeType($formatexportset);
+ top_httphead($mimetype, 1);
+ }
include DOL_DOCUMENT_ROOT.'/accountancy/tpl/export_journal.tpl.php';
+ if ($withAttachment == 1 && !empty($completefilename)) {
+ // create export file
+ $tmpDir = !empty($conf->accounting->multidir_temp[$conf->entity]) ? $conf->accounting->multidir_temp[$conf->entity] : $conf->accounting->dir_temp;
+ $exportFileFullName = $completefilename;
+ $exportFileBaseName = basename($exportFileFullName);
+ $exportFileName = pathinfo($exportFileBaseName, PATHINFO_FILENAME);
+ $exportFilePath = $tmpDir.'/'.$exportFileFullName;
+ $exportFile = fopen($exportFilePath, 'w');
+ if (!$exportFile) {
+ $this->errors[] = $langs->trans('ErrorFileNotFound', $exportFilePath);
+ return -1;
+ }
+ $archiveFileList[0] = array(
+ 'path' => $exportFilePath,
+ 'name' => $exportFileFullName,
+ );
+ // archive name and path
+ $archiveFullName = $exportFileName.'.zip';
+ $archivePath = $tmpDir.'/'.$archiveFullName;
+ }
switch ($formatexportset) {
case self::$EXPORT_TYPE_CONFIGURABLE:
@@ -345,7 +383,7 @@ class AccountancyExport
$this->exportCiel($TData);
break;
case self::$EXPORT_TYPE_QUADRATUS:
- $this->exportQuadratus($TData);
+ $archiveFileList = $this->exportQuadratus($TData, $exportFile, $archiveFileList, $withAttachment);
break;
case self::$EXPORT_TYPE_WINFIC:
$this->exportWinfic($TData);
@@ -399,6 +437,69 @@ class AccountancyExport
}
break;
}
+
+ // create and download export file or archive
+ if ($withAttachment == 1) {
+ $error = 0;
+
+ // close export file
+ if ($exportFile) {
+ fclose($exportFile);
+ }
+
+ if (!empty($archiveFullName) && !empty($archivePath) && !empty($archiveFileList)) {
+ // archive files
+ $downloadFileMimeType = 'application/zip';
+ $downloadFileFullName = $archiveFullName;
+ $downloadFilePath = $archivePath;
+
+ // create archive
+ $archive = new ZipArchive();
+ $res = $archive->open($archivePath, ZipArchive::OVERWRITE | ZipArchive::CREATE);
+ if ($res !== true) {
+ $error++;
+ $this->errors[] = $langs->trans('ErrorFileNotFound', $archivePath);
+ }
+ if (!$error) {
+ // add files
+ foreach ($archiveFileList as $archiveFileArr) {
+ $res = $archive->addFile($archiveFileArr['path'], $archiveFileArr['name']);
+ if (!$res) {
+ $error++;
+ $this->errors[] = $langs->trans('ErrorArchiveAddFile', $archiveFileArr['name']);
+ break;
+ }
+ }
+ }
+ if (!$error) {
+ // close archive
+ $archive->close();
+ }
+ } elseif (!empty($exportFileFullName) && !empty($exportFilePath)) {
+ // only one file to download
+ $downloadFileMimeType = 'text/csv';
+ $downloadFileFullName = $exportFileFullName;
+ $downloadFilePath = $exportFilePath;
+ }
+
+ if (!$error) {
+ // download export file
+ if (!empty($downloadFileMimeType) && !empty($downloadFileFullName) && !empty($downloadFilePath)) {
+ header('Content-Type: '.$downloadFileMimeType);
+ header('Content-Disposition: attachment; filename='.$downloadFileFullName);
+ header('Cache-Control: Public, must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: '.dol_filesize($downloadFilePath));
+ readfileLowMemory($downloadFilePath);
+ }
+ }
+
+ if ($error) {
+ return -1;
+ }
+ }
+
+ return 1;
}
@@ -584,10 +685,13 @@ class AccountancyExport
* Help : https://docplayer.fr/20769649-Fichier-d-entree-ascii-dans-quadracompta.html
* In QuadraCompta | Use menu : "Outils" > "Suivi des dossiers" > "Import ASCII(Compta)"
*
- * @param array $TData data
- * @return void
+ * @param array $TData Data
+ * @param resource $exportFile [=null] File resource to export or print if null
+ * @param array $archiveFileList [=array()] Archive file list : array of ['path', 'name']
+ * @param bool $withAttachment [=0] Not add files or 1 to have attached in an archive
+ * @return array Archive file list : array of ['path', 'name']
*/
- public function exportQuadratus(&$TData)
+ public function exportQuadratus(&$TData, $exportFile = null, $archiveFileList = array(), $withAttachment = 0)
{
global $conf, $db;
@@ -637,7 +741,11 @@ class AccountancyExport
$Tab['end_line'] = $end_line;
- print implode($Tab);
+ if ($exportFile) {
+ fwrite($exportFile, implode($Tab));
+ } else {
+ print implode($Tab);
+ }
}
$Tab = array();
@@ -708,12 +816,63 @@ class AccountancyExport
// We need to keep the 10 lastest number of invoice doc_ref not the beginning part that is the unusefull almost same part
// $Tab['num_piece3'] = str_pad(self::trunc($data->piece_num, 10), 10);
$Tab['num_piece3'] = substr(self::trunc($data->doc_ref, 20), -10);
- $Tab['filler4'] = str_repeat(' ', 73);
+ $Tab['reserved'] = str_repeat(' ', 10); // position 159
+ $Tab['currency_amount'] = str_repeat(' ', 13); // position 169
+ // get document file
+ $attachmentFileName = '';
+ if ($withAttachment == 1) {
+ $attachmentFileKey = trim($data->piece_num);
+ if (!isset($archiveFileList[$attachmentFileKey])) {
+ $objectDirPath = '';
+ $objectFileName = dol_sanitizeFileName($data->doc_ref);
+ if ($data->doc_type == 'customer_invoice') {
+ $objectDirPath = !empty($conf->facture->multidir_output[$conf->entity]) ? $conf->facture->multidir_output[$conf->entity] : $conf->facture->dir_output;
+ } elseif ($data->doc_type == 'expense_report') {
+ $objectDirPath = !empty($conf->expensereport->multidir_output[$conf->entity]) ? $conf->expensereport->multidir_output[$conf->entity] : $conf->factureexpensereport->dir_output;
+ } elseif ($data->doc_type == 'supplier_invoice') {
+ $objectDirPath = !empty($conf->fournisseur->facture->multidir_output[$conf->entity]) ? $conf->fournisseur->facture->multidir_output[$conf->entity] : $conf->fournisseur->facture->dir_output;
+ }
+ $arrayofinclusion = array();
+ $arrayofinclusion[] = '^'.preg_quote($objectFileName, '/').'\.pdf$';
+ $fileFoundList = dol_dir_list($objectDirPath.'/'.$objectFileName, 'files', 0, implode('|', $arrayofinclusion), '(\.meta|_preview.*\.png)$', 'date', SORT_DESC, 0, true);
+ if (!empty($fileFoundList)) {
+ $attachmentFileNameTrunc = str_pad(self::trunc($data->piece_num, 8), 8, '0', STR_PAD_LEFT);
+ foreach ($fileFoundList as $fileFound) {
+ if (strstr($fileFound['name'], $objectFileName)) {
+ $fileFoundPath = $objectDirPath.'/'.$objectFileName.'/'.$fileFound['name'];
+ if (file_exists($fileFoundPath)) {
+ $archiveFileList[$attachmentFileKey] = array(
+ 'path' => $fileFoundPath,
+ 'name' => $attachmentFileNameTrunc.'.pdf',
+ );
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (isset($archiveFileList[$attachmentFileKey])) {
+ $attachmentFileName = $archiveFileList[$attachmentFileKey]['name'];
+ }
+ }
+ if (dol_strlen($attachmentFileName) == 12) {
+ $Tab['attachment'] = $attachmentFileName; // position 182
+ } else {
+ $Tab['attachment'] = str_repeat(' ', 12); // position 182
+ }
+ $Tab['filler4'] = str_repeat(' ', 38);
$Tab['end_line'] = $end_line;
- print implode($Tab);
+ if ($exportFile) {
+ fwrite($exportFile, implode($Tab));
+ } else {
+ print implode($Tab);
+ }
}
+
+ return $archiveFileList;
}
/**
diff --git a/htdocs/accountancy/tpl/export_journal.tpl.php b/htdocs/accountancy/tpl/export_journal.tpl.php
index 22537a60a39..833b81fc8c0 100644
--- a/htdocs/accountancy/tpl/export_journal.tpl.php
+++ b/htdocs/accountancy/tpl/export_journal.tpl.php
@@ -33,7 +33,9 @@ $siren = getDolGlobalString('MAIN_INFO_SIREN');
$date_export = "_".dol_print_date(dol_now(), '%Y%m%d%H%M%S');
$endaccountingperiod = dol_print_date(dol_now(), '%Y%m%d');
-header('Content-Type: text/csv');
+if (!isset($withAttachments) || $withAttachments == 1) {
+ header('Content-Type: text/csv');
+}
include_once DOL_DOCUMENT_ROOT.'/accountancy/class/accountancyexport.class.php';
$accountancyexport = new AccountancyExport($db);
@@ -66,4 +68,6 @@ if (($accountancyexport->getFormatCode($formatexportset) == 'fec' || $accountanc
$completefilename = ($code ? $code."_" : "").($prefix ? $prefix."_" : "").$filename.($nodateexport ? "" : $date_export).".".$format;
}
-header('Content-Disposition: attachment;filename='.$completefilename);
+if (!isset($withAttachments) || $withAttachments == 1) {
+ header('Content-Disposition: attachment;filename=' . $completefilename);
+}
diff --git a/htdocs/langs/en_US/accountancy.lang b/htdocs/langs/en_US/accountancy.lang
index e988764d8ba..5ea3c692aee 100644
--- a/htdocs/langs/en_US/accountancy.lang
+++ b/htdocs/langs/en_US/accountancy.lang
@@ -342,6 +342,7 @@ ACCOUNTING_ENABLE_LETTERING=Enable the lettering function in the accounting
NotExportLettering=Do not export the lettering when generating the file
NotifiedExportDate=Flag exported lines as Exported (to modify a line, you will need to delete the whole transaction and re-transfert it into accounting)
NotifiedValidationDate=Validate and Lock the exported entries (same effect than the "%s" feature, modification and deletion of the lines will DEFINITELY not be possible)
+NotifiedExportFull=Export documents ?
DateValidationAndLock=Date validation and lock
ConfirmExportFile=Confirmation of the generation of the accounting export file ?
ExportDraftJournal=Export draft journal
@@ -442,6 +443,7 @@ AccountancyErrorMismatchLetterCode=Mismatch in reconcile code
AccountancyErrorMismatchBalanceAmount=The balance (%s) is not equal to 0
AccountancyErrorLetteringBookkeeping=Errors have occurred concerning the transactions: %s
ErrorAccountNumberAlreadyExists=The accounting number %s already exists
+ErrorArchiveAddFile=Can't put "%s" file in archive
## Import
ImportAccountingEntries=Accounting entries
diff --git a/htdocs/langs/fr_FR/accountancy.lang b/htdocs/langs/fr_FR/accountancy.lang
index 53edef4c0a1..0d2b7bb69a2 100644
--- a/htdocs/langs/fr_FR/accountancy.lang
+++ b/htdocs/langs/fr_FR/accountancy.lang
@@ -337,6 +337,7 @@ ACCOUNTING_DISABLE_BINDING_ON_EXPENSEREPORTS=Désactiver la liaison et le transf
## Export
NotifiedExportDate=Marquer les lignes exportées comme Exportées (pour modifier une ligne, vous devrez supprimer toute la transaction et la retransférer en comptabilité)
NotifiedValidationDate=Validez et verrouillez les entrées exportées (même effet que la fonctionnalité "%s", la modification et la suppression des lignes ne seront définitivement plus possibles)
+NotifiedExportFull=Exporter les pièces ?
DateValidationAndLock=Validation et verrouillage de la date
ConfirmExportFile=Confirmation de la génération du fichier d'export comptable ?
ExportDraftJournal=Exporter le journal brouillon
@@ -437,6 +438,7 @@ AccountancyErrorMismatchLetterCode=Non-concordance dans le code de réconciliati
AccountancyErrorMismatchBalanceAmount=Le solde (%s) n'est pas égal à 0
AccountancyErrorLetteringBookkeeping=Des erreurs sont survenues concernant les transactions : %s
ErrorAccountNumberAlreadyExists=Le code comptable %s existe déjà
+ErrorArchiveAddFile=Impossible d'ajouter le fichier "%s" dans l'archive
## Import
ImportAccountingEntries=Écritures comptables