2756 lines
107 KiB
PHP
2756 lines
107 KiB
PHP
<?php
|
|
/* Copyright (C) 2025 Frank Cools
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* \file core/class/declarationtva_pdf.class.php
|
|
* \ingroup declarationtva
|
|
* \brief PDF generation for CA-3 declarations
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
|
|
|
|
// Load TCPDF for PDF support (Dolibarr's standard)
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php';
|
|
|
|
// Load FPDI-TCPDF for fillable PDF support
|
|
require_once DOL_DOCUMENT_ROOT.'/custom/declarationtva/vendor/autoload.php';
|
|
|
|
/**
|
|
* Custom PDF class with footer functionality
|
|
*/
|
|
class DeclarationTVA_CustomPDF extends TCPDF
|
|
{
|
|
private $company_name = '';
|
|
private $total_pages = 0;
|
|
private $declaration_period = '';
|
|
|
|
public function setCompanyName($company_name)
|
|
{
|
|
$this->company_name = $company_name;
|
|
}
|
|
|
|
public function setDeclarationPeriod($period)
|
|
{
|
|
$this->declaration_period = $period;
|
|
}
|
|
|
|
public function setTotalPages($total_pages)
|
|
{
|
|
$this->total_pages = $total_pages;
|
|
}
|
|
|
|
public function getAliasNbPages()
|
|
{
|
|
return $this->total_pages > 0 ? $this->total_pages : parent::getAliasNbPages();
|
|
}
|
|
|
|
public function Footer()
|
|
{
|
|
// Set font size 7 (same as summary information)
|
|
$this->SetFont('helvetica', '', 7);
|
|
|
|
// Get current page number
|
|
$page_number = $this->getPage();
|
|
|
|
// Get page width for positioning
|
|
$page_width = $this->getPageWidth();
|
|
$margin_left = $this->getMargins()['left'];
|
|
$margin_right = $this->getMargins()['right'];
|
|
|
|
// Company name on the left
|
|
$this->SetXY($margin_left, $this->GetY());
|
|
$this->Cell(0, 6, $this->company_name, 0, 0, 'L');
|
|
|
|
// Declaration period centered
|
|
if (!empty($this->declaration_period)) {
|
|
$this->SetXY($margin_left, $this->GetY());
|
|
$this->Cell($page_width - $margin_left - $margin_right, 6, $this->declaration_period, 0, 0, 'C');
|
|
}
|
|
|
|
// Page number with total pages on the right using alias system
|
|
$this->SetXY($page_width - $margin_right - 30, $this->GetY());
|
|
$this->Cell(30, 6, 'Page ' . $page_number . '/' . $this->getAliasNbPages(), 0, 0, 'R');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class to generate CA-3 declaration PDF
|
|
*/
|
|
class DeclarationTVA_PDF
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error code (or message)
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var string[] Several error codes (or messages)
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var int Entity
|
|
*/
|
|
public $entity;
|
|
|
|
/**
|
|
* @var string Template path
|
|
*/
|
|
public $template_path;
|
|
|
|
/**
|
|
* @var string Template version (major.minor format)
|
|
*/
|
|
public $template_version = '30.2';
|
|
|
|
/**
|
|
* @var string Template document number
|
|
*/
|
|
public $template_document = '10963';
|
|
|
|
/**
|
|
* @var string Gitea repository URL for templates
|
|
*/
|
|
public $gitea_repo_url = 'https://git.covago.com/frank/DeclarationTVA';
|
|
|
|
/**
|
|
* @var string Manifest file URL
|
|
*/
|
|
public $manifest_url = 'https://git.covago.com/frank/DeclarationTVA/raw/branch/main/templates/manifest.json';
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
global $conf;
|
|
$this->db = $db;
|
|
$this->entity = (int) $conf->entity;
|
|
// Use absolute path to templates directory
|
|
$this->template_path = realpath(dirname(dirname(dirname(__FILE__)))) . '/templates/declarationtva/';
|
|
}
|
|
|
|
/**
|
|
* Generate complete CA-3 declaration PDF (CA-3 form + detailed pages)
|
|
*
|
|
* @param int $declaration_id Declaration ID
|
|
* @param string $output_path Output file path
|
|
* @param string $status Status for PDF generation (export/validation)
|
|
* @param string $outputlangs Output language
|
|
* @return string|false PDF file path or false on error
|
|
*/
|
|
public function generateCompleteCA3PDF($declaration_id, $output_path, $status = 'export', $outputlangs = '')
|
|
{
|
|
global $conf, $langs, $user;
|
|
|
|
// Load declaration data
|
|
$declaration = new DeclarationTVA($this->db);
|
|
$result = $declaration->fetch($declaration_id);
|
|
if ($result <= 0) {
|
|
$this->error = 'Declaration not found';
|
|
return false;
|
|
}
|
|
|
|
// Get CA-3 line data
|
|
$ca3_data = $declaration->getCA3Lines($declaration_id);
|
|
if (empty($ca3_data)) {
|
|
$this->error = 'No CA-3 data found';
|
|
return false;
|
|
}
|
|
|
|
// Get company information from Dolibarr's company configuration
|
|
global $mysoc;
|
|
|
|
// Debug logging
|
|
error_log("generateCompleteCA3PDF: Status=" . $status);
|
|
|
|
// Both export and validation should use the same logic
|
|
error_log("generateCompleteCA3PDF: Status is " . $status . ", checking for fillable template");
|
|
$template_file = $this->getTemplatePath();
|
|
if ($template_file && file_exists($template_file) && $this->isFillablePDF($template_file)) {
|
|
// Use fillable PDF template for both export and validation
|
|
error_log("generateCompleteCA3PDF: Using fillable PDF template");
|
|
$result = $this->fillFillablePDF($template_file, $output_path, $declaration, $ca3_data, $mysoc);
|
|
return $result ? $output_path : false;
|
|
}
|
|
|
|
// When no fillable template exists, use improved layout for both
|
|
error_log("generateCompleteCA3PDF: No fillable template found, using improved layout");
|
|
$result = $this->generateImprovedPDF($output_path, $declaration, $ca3_data, $mysoc);
|
|
error_log("generateCompleteCA3PDF: generateImprovedPDF result=" . ($result ? 'true' : 'false'));
|
|
return $result ? $output_path : false;
|
|
}
|
|
|
|
/**
|
|
* Generate improved PDF with complete layout (CA-3 form + detailed pages)
|
|
*
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $mysoc Company object
|
|
* @return bool Success
|
|
*/
|
|
private function generateImprovedPDF($output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Create a new PDF document
|
|
$pdf = new DeclarationTVA_CustomPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
|
|
|
// Set company name and declaration period for footer
|
|
$pdf->setCompanyName($mysoc->name);
|
|
$pdf->setDeclarationPeriod(dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'));
|
|
|
|
// Set document information
|
|
$pdf->SetCreator('DeclarationTVA Module');
|
|
$pdf->SetAuthor($mysoc->name);
|
|
$pdf->SetTitle('CA-3 Declaration ' . $declaration->declaration_number);
|
|
$pdf->SetSubject('French VAT Declaration');
|
|
|
|
// Set margins
|
|
$pdf->SetMargins(15, 15, 15);
|
|
$pdf->SetHeaderMargin(5);
|
|
$pdf->SetFooterMargin(10);
|
|
|
|
// Set thin borders for all elements
|
|
$pdf->SetLineWidth(0.1);
|
|
|
|
// Add a page
|
|
$pdf->AddPage();
|
|
|
|
// Add title
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->Cell(0, 10, 'Déclaration TVA CA-3', 0, 1, 'C');
|
|
$pdf->Ln(10);
|
|
|
|
// Add declaration information
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Numéro de déclaration: ' . $declaration->declaration_number, 0, 1);
|
|
$pdf->Cell(0, 8, 'Période: ' . dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'), 0, 1);
|
|
$pdf->Cell(0, 8, 'Statut: ' . $this->translateStatus($declaration->status), 0, 1);
|
|
$pdf->Ln(10);
|
|
|
|
// Add CA-3 sections
|
|
$this->addCA3Section($pdf, 'A. Opérations imposables', $ca3_data, array('A1', 'A2', 'A3', 'A4', 'A5'));
|
|
$this->addCA3Section($pdf, 'B. TVA due', $ca3_data, array('08', '09', '9B', '17'));
|
|
$this->addCA3Section($pdf, 'C. TVA déductible', $ca3_data, array('20', '21', '22'));
|
|
$this->addCA3Section($pdf, 'D. Résultat', $ca3_data, array('25', '26', '27', 'TD', '28', '32'));
|
|
|
|
// Add totals
|
|
$pdf->Ln(10);
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA COLLECTÉE: ' . price($declaration->total_vat_collected, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA DÉDUCTIBLE: ' . price($declaration->total_vat_deductible, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TVA NETTE DUE: ' . price($declaration->net_vat_due, 0, '', 1, 0), 0, 1);
|
|
|
|
if ($declaration->vat_credit > 0) {
|
|
$pdf->Cell(0, 8, 'CRÉDIT DE TVA: ' . price($declaration->vat_credit, 0, '', 1, 0), 0, 1);
|
|
}
|
|
|
|
// Add journal entry table
|
|
$this->addJournalEntryTable($pdf, $declaration, $ca3_data);
|
|
|
|
// Add bank journal entry table
|
|
$this->addBankJournalEntryTable($pdf, $declaration, $ca3_data);
|
|
|
|
// Add detailed breakdown pages using improved layout
|
|
$this->addDetailPages($pdf, $declaration, $ca3_data);
|
|
|
|
// Set total pages for footer after all pages are generated
|
|
$pdf->setTotalPages($pdf->getNumPages());
|
|
|
|
// Output PDF
|
|
$pdf->Output($output_path, 'F');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Improved PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate CA-3 declaration PDF (legacy method for backward compatibility)
|
|
*
|
|
* @param int $declaration_id Declaration ID
|
|
* @param string $outputlangs Output language
|
|
* @return string|false PDF file path or false on error
|
|
*/
|
|
public function generateCA3PDF($declaration_id, $outputlangs = '')
|
|
{
|
|
// Generate PDF filename
|
|
$filename = 'CA3_' . $declaration_id . '_' . date('Y-m-d') . '.pdf';
|
|
$filepath = DOL_DATA_ROOT . '/declarationtva/' . $filename;
|
|
|
|
// Ensure directory exists
|
|
if (!is_dir(DOL_DATA_ROOT . '/declarationtva/')) {
|
|
dol_mkdir(DOL_DATA_ROOT . '/declarationtva/');
|
|
}
|
|
|
|
// Use the new unified method
|
|
return $this->generateCompleteCA3PDF($declaration_id, $filepath, 'export', $outputlangs);
|
|
}
|
|
|
|
/**
|
|
* Generate detailed CA-3 declaration PDF with breakdown pages
|
|
*
|
|
* @param int $declaration_id Declaration ID
|
|
* @param string $outputlangs Output language
|
|
* @return string|false PDF file path or false on error
|
|
*/
|
|
public function generateDetailedCA3PDF($declaration_id, $outputlangs = '')
|
|
{
|
|
// Generate PDF filename
|
|
$filename = 'CA3_' . $declaration_id . '_' . date('Y-m-d') . '.pdf';
|
|
|
|
// Create VAT declarations documents directory structure (same as saveValidatedPDF expects)
|
|
$vat_declarations_dir = DOL_DATA_ROOT . '/documents/declarationtva/';
|
|
$year_dir = $vat_declarations_dir . date('Y') . '/';
|
|
$month_dir = $year_dir . date('m') . '/';
|
|
|
|
// Create directories if they don't exist
|
|
if (!is_dir($vat_declarations_dir)) {
|
|
dol_mkdir($vat_declarations_dir);
|
|
}
|
|
if (!is_dir($year_dir)) {
|
|
dol_mkdir($year_dir);
|
|
}
|
|
if (!is_dir($month_dir)) {
|
|
dol_mkdir($month_dir);
|
|
}
|
|
|
|
$filepath = $month_dir . $filename;
|
|
|
|
// Debug logging
|
|
error_log("generateDetailedCA3PDF: Calling generateCompleteCA3PDF with status='validation'");
|
|
error_log("generateDetailedCA3PDF: Output path=" . $filepath);
|
|
|
|
// Use the new unified method for validation
|
|
$result = $this->generateCompleteCA3PDF($declaration_id, $filepath, 'validation', $outputlangs);
|
|
|
|
error_log("generateDetailedCA3PDF: Result=" . ($result ? $result : 'false'));
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get the template file path (custom or default)
|
|
*
|
|
* @return string|false Template file path or false if not found
|
|
*/
|
|
private function getTemplatePath()
|
|
{
|
|
// Check for custom template first
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
|
|
// Fall back to default template
|
|
$default_template = $this->template_path . 'ca3_official_template.pdf';
|
|
if (file_exists($default_template)) {
|
|
return $default_template;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Fill PDF template with declaration data
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillPDFTemplate($template_path, $output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Check if we have a custom fillable PDF template
|
|
if (file_exists($template_path) && $this->isFillablePDF($template_path)) {
|
|
return $this->fillFillablePDF($template_path, $output_path, $declaration, $ca3_data, $mysoc);
|
|
} else {
|
|
// Fall back to basic PDF generation
|
|
return $this->generateBasicPDF($output_path, $declaration, $ca3_data, $mysoc);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->error = 'PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if PDF is fillable (has form fields)
|
|
*
|
|
* @param string $pdf_path PDF file path
|
|
* @return bool True if fillable
|
|
*/
|
|
private function isFillablePDF($pdf_path)
|
|
{
|
|
// Check if it's our template (custom or official)
|
|
return strpos($pdf_path, 'ca3_custom_template.pdf') !== false ||
|
|
strpos($pdf_path, 'ca3_official_template.pdf') !== false;
|
|
}
|
|
|
|
/**
|
|
* Fill a fillable PDF template using pdftk
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillFillablePDF($template_path, $output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Check if pdftk is available
|
|
if (!$this->isPdftkAvailable()) {
|
|
// Fallback to manual approach if pdftk is not available
|
|
return $this->fillFillablePDFManual($template_path, $output_path, $declaration, $ca3_data, $mysoc);
|
|
}
|
|
|
|
// Prepare field data mapping
|
|
$field_data = $this->prepareFieldData($declaration, $ca3_data, $mysoc);
|
|
|
|
// Create FDF data file for pdftk
|
|
$fdf_file = $this->createFDFFile($field_data, $declaration);
|
|
|
|
// Use pdftk to fill the form
|
|
$pdftk_path = $this->getPdftkPath();
|
|
$command = "\"$pdftk_path\" \"$template_path\" fill_form \"$fdf_file\" output \"$output_path\"";
|
|
$result = shell_exec($command . ' 2>&1');
|
|
|
|
// Clean up temporary FDF file
|
|
if (file_exists($fdf_file)) {
|
|
unlink($fdf_file);
|
|
}
|
|
|
|
// Check if output file was created successfully
|
|
if (file_exists($output_path) && filesize($output_path) > 0) {
|
|
// Add detailed pages to the filled PDF
|
|
$this->addDetailedPagesToPDF($output_path, $declaration, $ca3_data, $mysoc);
|
|
return true;
|
|
} else {
|
|
$this->error = 'pdftk failed to generate output: ' . $result;
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to fill PDF template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare field data for PDF form filling
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return array Field data mapping
|
|
*/
|
|
private function prepareFieldData($declaration, $ca3_data, $mysoc)
|
|
{
|
|
$field_data = array();
|
|
|
|
// Get the actual company information from Dolibarr configuration
|
|
global $conf;
|
|
|
|
// Use Dolibarr's company configuration
|
|
$field_data['company_name'] = $mysoc->name;
|
|
$field_data['company_address'] = $mysoc->address;
|
|
$field_data['company_city'] = $mysoc->town;
|
|
$field_data['company_postal_code'] = $mysoc->zip;
|
|
|
|
|
|
// Try different fields for SIRET - it could be in different idprof fields
|
|
$siret_value = !empty($mysoc->idprof2) ? $mysoc->idprof2 :
|
|
(!empty($mysoc->idprof1) ? $mysoc->idprof1 :
|
|
(!empty($mysoc->idprof3) ? $mysoc->idprof3 :
|
|
(!empty($mysoc->idprof4) ? $mysoc->idprof4 : '')));
|
|
|
|
// If no SIRET found, use a placeholder
|
|
if (empty($siret_value)) {
|
|
$siret_value = 'SIRET_NOT_CONFIGURED';
|
|
}
|
|
|
|
$field_data['company_siret'] = $this->formatSiret($siret_value);
|
|
|
|
$field_data['company_vat_number'] = $this->formatVatNumber($mysoc->tva_intra); // VAT number
|
|
$field_data['declaration_period_start'] = dol_print_date($declaration->start_date, 'day');
|
|
$field_data['declaration_period_end'] = dol_print_date($declaration->end_date, 'day');
|
|
$field_data['declaration_number'] = $declaration->declaration_number;
|
|
|
|
// Section A: Opérations imposables
|
|
$field_data['A1_amount'] = $this->getCA3LineAmount($ca3_data, 'A1');
|
|
$field_data['A2_amount'] = $this->getCA3LineAmount($ca3_data, 'A2');
|
|
$field_data['A3_amount'] = $this->getCA3LineAmount($ca3_data, 'A3');
|
|
$field_data['A4_amount'] = $this->getCA3LineAmount($ca3_data, 'A4');
|
|
$field_data['A5_amount'] = $this->getCA3LineAmount($ca3_data, 'A5');
|
|
|
|
// Section A: Additional reference fields (E1-E6, F1-F2, F6-F8)
|
|
$field_data['E1_amount'] = $this->getCA3LineAmount($ca3_data, 'E1');
|
|
$field_data['E2_amount'] = $this->getCA3LineAmount($ca3_data, 'E2');
|
|
$field_data['E3_amount'] = $this->getCA3LineAmount($ca3_data, 'E3');
|
|
$field_data['E4_amount'] = $this->getCA3LineAmount($ca3_data, 'E4');
|
|
$field_data['E5_amount'] = $this->getCA3LineAmount($ca3_data, 'E5');
|
|
$field_data['E6_amount'] = $this->getCA3LineAmount($ca3_data, 'E6');
|
|
$field_data['F1_amount'] = $this->getCA3LineAmount($ca3_data, 'F1');
|
|
$field_data['F2_amount'] = $this->getCA3LineAmount($ca3_data, 'F2');
|
|
$field_data['F6_amount'] = $this->getCA3LineAmount($ca3_data, 'F6');
|
|
$field_data['F7_amount'] = $this->getCA3LineAmount($ca3_data, 'F7');
|
|
$field_data['F8_amount'] = $this->getCA3LineAmount($ca3_data, 'F8');
|
|
|
|
|
|
|
|
// Section B: TVA due (Base + VAT columns)
|
|
$field_data['B08_base_amount'] = $this->getCA3LineAmount($ca3_data, '08', 'base_amount');
|
|
$field_data['B08_vat_amount'] = $this->getCA3LineAmount($ca3_data, '08', 'vat_amount');
|
|
$field_data['B09_base_amount'] = $this->getCA3LineAmount($ca3_data, '09', 'base_amount');
|
|
$field_data['B09_vat_amount'] = $this->getCA3LineAmount($ca3_data, '09', 'vat_amount');
|
|
$field_data['B9B_base_amount'] = $this->getCA3LineAmount($ca3_data, '9B', 'base_amount');
|
|
$field_data['B9B_vat_amount'] = $this->getCA3LineAmount($ca3_data, '9B', 'vat_amount');
|
|
$field_data['B17_amount'] = $this->getCA3LineAmount($ca3_data, '17');
|
|
|
|
// Section C: TVA déductible
|
|
$field_data['C20_amount'] = $this->getCA3LineAmount($ca3_data, '20');
|
|
$field_data['C21_amount'] = $this->getCA3LineAmount($ca3_data, '21');
|
|
$field_data['C22_amount'] = $this->getCA3LineAmount($ca3_data, '22');
|
|
|
|
// Section F: Intracom Acquisitions (already mapped above in Section A)
|
|
|
|
// Section D: Résultat (Calculated)
|
|
$field_data['D25_amount'] = $this->getCA3LineAmount($ca3_data, '25');
|
|
$field_data['D26_amount'] = $this->getCA3LineAmount($ca3_data, '26');
|
|
$field_data['D27_amount'] = $this->getCA3LineAmount($ca3_data, '27');
|
|
$field_data['DTD_amount'] = $this->getCA3LineAmount($ca3_data, 'TD');
|
|
$field_data['D28_amount'] = $this->getCA3LineAmount($ca3_data, '28');
|
|
$field_data['D32_amount'] = $this->getCA3LineAmount($ca3_data, '32');
|
|
|
|
// Subtotals
|
|
$field_data['subtotal_B16_amount'] = $this->getCA3LineAmount($ca3_data, '16');
|
|
$field_data['subtotal_C23_amount'] = $this->getCA3LineAmount($ca3_data, '23');
|
|
|
|
// Grand totals
|
|
$field_data['total_vat_collected'] = $this->formatAmount($declaration->total_vat_collected);
|
|
$field_data['total_vat_deductible'] = $this->formatAmount($declaration->total_vat_deductible);
|
|
$field_data['net_vat_due'] = $this->formatAmount($declaration->net_vat_due);
|
|
$field_data['vat_credit'] = $this->formatAmount($declaration->vat_credit);
|
|
|
|
|
|
return $field_data;
|
|
}
|
|
|
|
/**
|
|
* Format amount for PDF display
|
|
*
|
|
* @param float $amount Amount to format
|
|
* @return string Formatted amount
|
|
*/
|
|
private function formatAmount($amount)
|
|
{
|
|
return number_format($amount, 0, ',', ' ');
|
|
}
|
|
|
|
/**
|
|
* Format amount for display with real values (with decimals)
|
|
*
|
|
* @param float $amount Amount to format
|
|
* @return string Formatted amount with decimals
|
|
*/
|
|
private function formatAmountReal($amount)
|
|
{
|
|
return number_format($amount, 2, ',', ' ');
|
|
}
|
|
|
|
/**
|
|
* Fill a PDF form field by modifying PDF content
|
|
*
|
|
* @param string $pdf_content PDF content
|
|
* @param string $field_name Field name
|
|
* @param string $value Field value
|
|
* @return string Modified PDF content
|
|
*/
|
|
private function fillPDFFormField($pdf_content, $field_name, $value)
|
|
{
|
|
// Escape special characters in the value
|
|
$escaped_value = $this->escapePDFString($value);
|
|
|
|
// Look for the field in the PDF content and replace its value
|
|
// This is a simplified approach that works for many PDF types
|
|
|
|
// Pattern 1: Look for /T(field_name) and add /V(value) after it
|
|
$pattern1 = '/\/T\s*\(\s*' . preg_quote($field_name, '/') . '\s*\)/';
|
|
if (preg_match($pattern1, $pdf_content)) {
|
|
$pdf_content = preg_replace(
|
|
$pattern1,
|
|
'/T(' . $field_name . ') /V(' . $escaped_value . ')',
|
|
$pdf_content
|
|
);
|
|
}
|
|
|
|
// Pattern 2: Look for existing /V values and replace them
|
|
$pattern2 = '/\/T\s*\(\s*' . preg_quote($field_name, '/') . '\s*\).*?\/V\s*\([^)]*\)/';
|
|
if (preg_match($pattern2, $pdf_content)) {
|
|
$pdf_content = preg_replace(
|
|
$pattern2,
|
|
'/T(' . $field_name . ') /V(' . $escaped_value . ')',
|
|
$pdf_content
|
|
);
|
|
}
|
|
|
|
return $pdf_content;
|
|
}
|
|
|
|
/**
|
|
* Check if pdftk is available on the system
|
|
*
|
|
* @return bool True if pdftk is available
|
|
*/
|
|
private function isPdftkAvailable()
|
|
{
|
|
// Try multiple common locations for pdftk
|
|
$possible_paths = array(
|
|
'/usr/bin/pdftk',
|
|
'/usr/local/bin/pdftk',
|
|
'/bin/pdftk',
|
|
'/opt/pdftk/bin/pdftk',
|
|
'/usr/share/pdftk/bin/pdftk',
|
|
'/home/*/bin/pdftk',
|
|
'/home/*/.local/bin/pdftk'
|
|
);
|
|
|
|
foreach ($possible_paths as $path) {
|
|
if (strpos($path, '*') !== false) {
|
|
// Handle glob patterns
|
|
$glob_paths = glob($path);
|
|
foreach ($glob_paths as $glob_path) {
|
|
if (file_exists($glob_path) && is_executable($glob_path)) {
|
|
$test_result = shell_exec("$glob_path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (file_exists($path) && is_executable($path)) {
|
|
$test_result = shell_exec("$path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try 'which pdftk' in case it's in PATH
|
|
$result = shell_exec('which pdftk 2>/dev/null');
|
|
if (!empty(trim($result))) {
|
|
$test_result = shell_exec(trim($result) . ' --version 2>&1');
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the full path to pdftk executable
|
|
*
|
|
* @return string Path to pdftk executable
|
|
*/
|
|
private function getPdftkPath()
|
|
{
|
|
// Try multiple common locations for pdftk
|
|
$possible_paths = array(
|
|
'/usr/bin/pdftk',
|
|
'/usr/local/bin/pdftk',
|
|
'/bin/pdftk',
|
|
'/opt/pdftk/bin/pdftk',
|
|
'/usr/share/pdftk/bin/pdftk',
|
|
'/home/*/bin/pdftk',
|
|
'/home/*/.local/bin/pdftk'
|
|
);
|
|
|
|
foreach ($possible_paths as $path) {
|
|
if (strpos($path, '*') !== false) {
|
|
// Handle glob patterns
|
|
$glob_paths = glob($path);
|
|
foreach ($glob_paths as $glob_path) {
|
|
if (file_exists($glob_path) && is_executable($glob_path)) {
|
|
$test_result = shell_exec("$glob_path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return $glob_path;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (file_exists($path) && is_executable($path)) {
|
|
$test_result = shell_exec("$path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return $path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try 'which pdftk' in case it's in PATH
|
|
$result = shell_exec('which pdftk 2>/dev/null');
|
|
if (!empty(trim($result))) {
|
|
$test_result = shell_exec(trim($result) . ' --version 2>&1');
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return trim($result);
|
|
}
|
|
}
|
|
|
|
// Default fallback
|
|
return 'pdftk';
|
|
}
|
|
|
|
/**
|
|
* Create FDF file for pdftk form filling
|
|
*
|
|
* @param array $field_data Field data
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @return string FDF file path
|
|
*/
|
|
private function createFDFFile($field_data, $declaration)
|
|
{
|
|
$fdf_file = sys_get_temp_dir() . '/ca3_fdf_' . $declaration->declaration_number . '_' . uniqid() . '.fdf';
|
|
|
|
$fdf_content = "%FDF-1.2\n";
|
|
$fdf_content .= "1 0 obj\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/FDF\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/Fields [\n";
|
|
|
|
foreach ($field_data as $field_name => $value) {
|
|
if (!empty($value)) {
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/T (" . $field_name . ")\n";
|
|
$fdf_content .= "/V (" . $this->escapeFDFString($value) . ")\n";
|
|
|
|
// Force Courier New 9pt for official documents
|
|
$fdf_content .= "/Ff 0\n"; // Field flags (0 = no special flags)
|
|
$fdf_content .= "/F 9\n"; // Font size 9pt
|
|
$fdf_content .= "/DA (/Cour 9 Tf 0 g)\n"; // Courier 9pt, black
|
|
$fdf_content .= "/Q 0\n"; // Left alignment
|
|
$fdf_content .= ">>\n";
|
|
}
|
|
}
|
|
|
|
$fdf_content .= "]\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= "endobj\n";
|
|
$fdf_content .= "trailer\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/Root 1 0 R\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= "%%EOF\n";
|
|
|
|
file_put_contents($fdf_file, $fdf_content);
|
|
return $fdf_file;
|
|
}
|
|
|
|
/**
|
|
* Escape string for FDF format
|
|
*
|
|
* @param string $value Value to escape
|
|
* @return string Escaped value
|
|
*/
|
|
private function escapeFDFString($value)
|
|
{
|
|
// Escape special FDF characters
|
|
$value = str_replace('\\', '\\\\', $value);
|
|
$value = str_replace('(', '\\(', $value);
|
|
$value = str_replace(')', '\\)', $value);
|
|
return $value;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Manual PDF filling fallback (when pdftk is not available)
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillFillablePDFManual($template_path, $output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Simple approach: Just copy the template and create a data file
|
|
if (copy($template_path, $output_path)) {
|
|
// Create a data file with all the values for manual filling
|
|
$data_file = dirname($output_path) . '/ca3_data_' . $declaration->declaration_number . '.txt';
|
|
$field_data = $this->prepareFieldData($declaration, $ca3_data, $mysoc);
|
|
|
|
$data_content = "CA-3 Declaration Data (Manual Filling Required)\n";
|
|
$data_content .= "============================================\n\n";
|
|
$data_content .= "Note: pdftk is not installed. Please install pdftk for automatic form filling.\n";
|
|
$data_content .= "See installation guide: /custom/declarationtva/docs/PDFTK_INSTALLATION.md\n\n";
|
|
|
|
$data_content .= "Declaration: " . $declaration->declaration_number . "\n";
|
|
$data_content .= "Period: " . dol_print_date($declaration->start_date, 'day') . " - " . dol_print_date($declaration->end_date, 'day') . "\n\n";
|
|
|
|
$data_content .= "Company Information:\n";
|
|
$data_content .= "-------------------\n";
|
|
$data_content .= "company_name: " . $field_data['company_name'] . "\n";
|
|
$data_content .= "company_address: " . $field_data['company_address'] . "\n";
|
|
$data_content .= "company_city: " . $field_data['company_city'] . "\n";
|
|
$data_content .= "company_postal_code: " . $field_data['company_postal_code'] . "\n";
|
|
$data_content .= "company_siret: " . $field_data['company_siret'] . "\n\n";
|
|
|
|
$data_content .= "Declaration Information:\n";
|
|
$data_content .= "------------------------\n";
|
|
$data_content .= "declaration_period_start: " . $field_data['declaration_period_start'] . "\n";
|
|
$data_content .= "declaration_period_end: " . $field_data['declaration_period_end'] . "\n";
|
|
$data_content .= "declaration_number: " . $field_data['declaration_number'] . "\n\n";
|
|
|
|
$data_content .= "Section A - Opérations imposables:\n";
|
|
$data_content .= "----------------------------------\n";
|
|
$data_content .= "A1_amount: " . $field_data['A1_amount'] . "\n";
|
|
$data_content .= "A2_amount: " . $field_data['A2_amount'] . "\n";
|
|
$data_content .= "A3_amount: " . $field_data['A3_amount'] . "\n";
|
|
$data_content .= "A4_amount: " . $field_data['A4_amount'] . "\n";
|
|
$data_content .= "A5_amount: " . $field_data['A5_amount'] . "\n";
|
|
$data_content .= "E1_amount: " . $field_data['E1_amount'] . "\n";
|
|
$data_content .= "E2_amount: " . $field_data['E2_amount'] . "\n";
|
|
$data_content .= "E3_amount: " . $field_data['E3_amount'] . "\n";
|
|
$data_content .= "E4_amount: " . $field_data['E4_amount'] . "\n";
|
|
$data_content .= "E5_amount: " . $field_data['E5_amount'] . "\n";
|
|
$data_content .= "E6_amount: " . $field_data['E6_amount'] . "\n";
|
|
$data_content .= "F1_amount: " . $field_data['F1_amount'] . "\n";
|
|
$data_content .= "F2_amount: " . $field_data['F2_amount'] . "\n";
|
|
$data_content .= "F6_amount: " . $field_data['F6_amount'] . "\n";
|
|
$data_content .= "F7_amount: " . $field_data['F7_amount'] . "\n";
|
|
$data_content .= "F8_amount: " . $field_data['F8_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section B - TVA due:\n";
|
|
$data_content .= "--------------------\n";
|
|
$data_content .= "B08_base_amount: " . $field_data['B08_base_amount'] . "\n";
|
|
$data_content .= "B08_vat_amount: " . $field_data['B08_vat_amount'] . "\n";
|
|
$data_content .= "B09_base_amount: " . $field_data['B09_base_amount'] . "\n";
|
|
$data_content .= "B09_vat_amount: " . $field_data['B09_vat_amount'] . "\n";
|
|
$data_content .= "B9B_base_amount: " . $field_data['B9B_base_amount'] . "\n";
|
|
$data_content .= "B9B_vat_amount: " . $field_data['B9B_vat_amount'] . "\n";
|
|
$data_content .= "B17_amount: " . $field_data['B17_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section C - TVA déductible:\n";
|
|
$data_content .= "----------------------------\n";
|
|
$data_content .= "C20_amount: " . $field_data['C20_amount'] . "\n";
|
|
$data_content .= "C21_amount: " . $field_data['C21_amount'] . "\n";
|
|
$data_content .= "C22_amount: " . $field_data['C22_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section D - Résultat:\n";
|
|
$data_content .= "---------------------\n";
|
|
$data_content .= "D25_amount: " . $field_data['D25_amount'] . "\n";
|
|
$data_content .= "D26_amount: " . $field_data['D26_amount'] . "\n";
|
|
$data_content .= "DTD_amount: " . $field_data['DTD_amount'] . "\n";
|
|
$data_content .= "D28_amount: " . $field_data['D28_amount'] . "\n";
|
|
$data_content .= "D29_amount: " . $field_data['D29_amount'] . "\n\n";
|
|
|
|
$data_content .= "Totals:\n";
|
|
$data_content .= "-------\n";
|
|
$data_content .= "total_vat_collected: " . $field_data['total_vat_collected'] . "\n";
|
|
$data_content .= "total_vat_deductible: " . $field_data['total_vat_deductible'] . "\n";
|
|
$data_content .= "net_vat_due: " . $field_data['net_vat_due'] . "\n";
|
|
$data_content .= "vat_credit: " . $field_data['vat_credit'] . "\n";
|
|
|
|
file_put_contents($data_file, $data_content);
|
|
|
|
// Add detailed pages to the filled PDF
|
|
$this->addDetailedPagesToPDF($output_path, $declaration, $ca3_data, $mysoc);
|
|
|
|
return true;
|
|
} else {
|
|
$this->error = 'Failed to copy template file';
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to fill PDF template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate basic PDF (fallback method)
|
|
*
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function generateBasicPDF($output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Create a new PDF document
|
|
$pdf = new DeclarationTVA_CustomPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
|
|
|
// Set company name and declaration period for footer
|
|
$pdf->setCompanyName($mysoc->name);
|
|
$pdf->setDeclarationPeriod(dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'));
|
|
|
|
// Set document information
|
|
$pdf->SetCreator('DeclarationTVA Module');
|
|
$pdf->SetAuthor($mysoc->name);
|
|
$pdf->SetTitle('CA-3 Declaration ' . $declaration->declaration_number);
|
|
$pdf->SetSubject('French VAT Declaration');
|
|
|
|
// Set margins
|
|
$pdf->SetMargins(15, 15, 15);
|
|
$pdf->SetHeaderMargin(5);
|
|
$pdf->SetFooterMargin(10);
|
|
|
|
// Set thin borders for all elements
|
|
$pdf->SetLineWidth(0.1);
|
|
|
|
// Add a page
|
|
$pdf->AddPage();
|
|
|
|
// Add title
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->Cell(0, 10, 'Déclaration TVA CA-3', 0, 1, 'C');
|
|
$pdf->Ln(10);
|
|
|
|
// Add declaration information
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Numéro de déclaration: ' . $declaration->declaration_number, 0, 1);
|
|
$pdf->Cell(0, 8, 'Période: ' . dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'), 0, 1);
|
|
$pdf->Cell(0, 8, 'Statut: ' . $this->translateStatus($declaration->status), 0, 1);
|
|
$pdf->Ln(10);
|
|
|
|
// Add CA-3 sections
|
|
$this->addCA3Section($pdf, 'A. Opérations imposables', $ca3_data, array('A1', 'A2', 'A3', 'A4', 'A5'));
|
|
$this->addCA3Section($pdf, 'B. TVA due', $ca3_data, array('08', '09', '9B', '17'));
|
|
$this->addCA3Section($pdf, 'C. TVA déductible', $ca3_data, array('20', '21', '22'));
|
|
$this->addCA3Section($pdf, 'D. Résultat', $ca3_data, array('25', '26', '27', 'TD', '28', '32'));
|
|
|
|
// Add totals
|
|
$pdf->Ln(10);
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA COLLECTÉE: ' . price($declaration->total_vat_collected, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA DÉDUCTIBLE: ' . price($declaration->total_vat_deductible, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TVA NETTE DUE: ' . price($declaration->net_vat_due, 0, '', 1, 0), 0, 1);
|
|
|
|
if ($declaration->vat_credit > 0) {
|
|
$pdf->Cell(0, 8, 'CRÉDIT DE TVA: ' . price($declaration->vat_credit, 0, '', 1, 0), 0, 1);
|
|
}
|
|
|
|
// Add detailed breakdown pages using improved layout
|
|
$this->addDetailPages($pdf, $declaration, $ca3_data);
|
|
|
|
// Set total pages for footer after all pages are generated
|
|
$pdf->setTotalPages($pdf->getNumPages());
|
|
|
|
// Output PDF
|
|
$pdf->Output($output_path, 'F');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Basic PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add CA-3 section to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param string $section_title Section title
|
|
* @param array $ca3_data CA-3 data
|
|
* @param array $lines Lines to include
|
|
*/
|
|
private function addCA3Section($pdf, $section_title, $ca3_data, $lines)
|
|
{
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, $section_title, 0, 1);
|
|
|
|
$pdf->SetFont('helvetica', '', 10);
|
|
foreach ($lines as $line) {
|
|
if (isset($ca3_data[$line])) {
|
|
$data = $ca3_data[$line];
|
|
$amount = isset($data['vat_amount']) ? $data['vat_amount'] : 0;
|
|
|
|
// Add link to detail page for this line
|
|
$link = $pdf->AddLink();
|
|
$pdf->SetLink($link, 0, $this->getDetailPageNumber($line));
|
|
|
|
$pdf->Cell(20, 6, $line, 1, 0, 'C', false, $link);
|
|
$pdf->Cell(100, 6, $data['line_label'], 1, 0);
|
|
$pdf->Cell(30, 6, price($amount, 0, '', 1, 0), 1, 1, 'R');
|
|
}
|
|
}
|
|
$pdf->Ln(5);
|
|
}
|
|
|
|
/**
|
|
* Add detailed breakdown pages to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
*/
|
|
private function addDetailPages($pdf, $declaration, $ca3_data)
|
|
{
|
|
// Define calculated lines that should be excluded from detailed breakdown
|
|
$calculated_lines = array('25', '26', '27', 'TD', '28', '32', '16', '23');
|
|
|
|
// Define the order of lines as they appear in the view page
|
|
$ordered_lines = array(
|
|
// Section A: Opérations imposables
|
|
'A1', 'A2', 'A3', 'A4', 'A5', 'E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'F1', 'F2', 'F6', 'F7', 'F8',
|
|
// Section B: TVA due
|
|
'08', '09', '9B', '17',
|
|
// Section C: TVA déductible
|
|
'19', '20', '21', '22'
|
|
);
|
|
|
|
// Create a lookup array for quick access to line data
|
|
$ca3_lookup = array();
|
|
foreach ($ca3_data as $line_data) {
|
|
$ca3_lookup[$line_data['ca3_line']] = $line_data;
|
|
}
|
|
|
|
// Get lines that have actual values and are not calculated lines, in the correct order
|
|
$lines_with_data = array();
|
|
foreach ($ordered_lines as $line_code) {
|
|
// Skip if line doesn't exist in data
|
|
if (!isset($ca3_lookup[$line_code])) {
|
|
continue;
|
|
}
|
|
|
|
// Skip calculated lines - they don't have account mappings
|
|
if (in_array($line_code, $calculated_lines)) {
|
|
continue;
|
|
}
|
|
|
|
$line_data = $ca3_lookup[$line_code];
|
|
$has_vat_amount = isset($line_data['vat_amount']) && $line_data['vat_amount'] > 0;
|
|
$has_base_amount = isset($line_data['base_amount']) && $line_data['base_amount'] > 0;
|
|
$has_total_amount = isset($line_data['total_amount']) && $line_data['total_amount'] > 0;
|
|
|
|
// Only include lines that have meaningful values and are not calculated
|
|
if ($has_vat_amount || $has_base_amount || $has_total_amount) {
|
|
$lines_with_data[] = $line_code;
|
|
}
|
|
}
|
|
|
|
// If no lines have data, add a message page
|
|
if (empty($lines_with_data)) {
|
|
$pdf->AddPage();
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->Cell(0, 10, 'Aucune donnée détaillée disponible', 0, 1, 'C');
|
|
$pdf->Ln(10);
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Aucune ligne de la déclaration ne contient de données comptables.', 0, 1);
|
|
$pdf->Cell(0, 8, 'Veuillez configurer les mappings de comptes dans la section Administration.', 0, 1);
|
|
return;
|
|
}
|
|
|
|
// Start line details on page 2
|
|
$pdf->AddPage();
|
|
|
|
// Add a detail section for each line with data in the correct order
|
|
foreach ($lines_with_data as $line_code) {
|
|
$this->addLineDetailPage($pdf, $declaration, $line_code);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a detail section for a specific CA-3 line
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param string $line_code CA-3 line code
|
|
*/
|
|
private function addLineDetailPage($pdf, $declaration, $line_code)
|
|
{
|
|
// Check if we need a new page (if current position is too low)
|
|
$current_y = $pdf->GetY();
|
|
$page_height = $pdf->getPageHeight() - $pdf->getMargins()['bottom'];
|
|
|
|
// If we're too close to the bottom, start a new page
|
|
if ($current_y > $page_height - 100) {
|
|
$pdf->AddPage();
|
|
} else {
|
|
// Add some space between sections
|
|
$pdf->Ln(10);
|
|
}
|
|
|
|
// Set section title with light gray background
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->SetTextColor(0, 0, 0);
|
|
$pdf->SetFillColor(240, 240, 240); // Light gray background
|
|
$pdf->Cell(0, 10, 'Détail de la ligne ' . $line_code, 1, 1, 'C', true);
|
|
|
|
// Add separator line (thin border)
|
|
$pdf->SetDrawColor(0, 0, 0);
|
|
$pdf->SetLineWidth(0.1); // Thin border
|
|
$pdf->Line($pdf->getMargins()['left'], $pdf->GetY(), $pdf->getPageWidth() - $pdf->getMargins()['right'], $pdf->GetY());
|
|
$pdf->Ln(5);
|
|
|
|
// Get line details
|
|
$line_details = $declaration->getCA3LineDetails($declaration->rowid, $line_code);
|
|
|
|
if (empty($line_details) || empty($line_details['account_details'])) {
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Aucun détail comptable disponible pour cette ligne.', 0, 1);
|
|
$pdf->Cell(0, 8, 'Cette ligne peut être calculée automatiquement ou ne pas avoir de mapping de comptes.', 0, 1);
|
|
|
|
// Still show the calculated value if available
|
|
if (!empty($line_details['calculated_line'])) {
|
|
$calc = $line_details['calculated_line'];
|
|
$pdf->Ln(5);
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, 'Valeur calculée:', 0, 1);
|
|
$pdf->SetFont('helvetica', '', 10);
|
|
if ($calc->base_amount > 0) {
|
|
$pdf->Cell(0, 6, 'Montant de base: ' . price($calc->base_amount, 0, '', 1, 0), 0, 1);
|
|
}
|
|
if ($calc->vat_amount > 0) {
|
|
$pdf->Cell(0, 6, 'Montant de TVA: ' . price($calc->vat_amount, 0, '', 1, 0), 0, 1);
|
|
}
|
|
if ($calc->total_amount > 0) {
|
|
$pdf->Cell(0, 6, 'Total: ' . price($calc->total_amount, 0, '', 1, 0), 0, 1);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Add line summary information (without title) - using table font size
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->Cell(0, 6, 'Période: ' . dol_print_date($line_details['start_date'], 'day') . ' - ' . dol_print_date($line_details['end_date'], 'day'), 0, 1);
|
|
|
|
// Special handling for lines with both base and VAT accounts (08, 09, 9B)
|
|
if (in_array($line_code, array('08', '09', '9B'))) {
|
|
// Count base and VAT accounts separately (only non-zero amounts)
|
|
$base_count = 0;
|
|
$vat_count = 0;
|
|
foreach ($line_details['account_details'] as $account) {
|
|
// Only count accounts with non-zero amounts
|
|
$has_amount = false;
|
|
if (isset($account['base_amount']) && $account['base_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['vat_amount']) && $account['vat_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['total_amount']) && $account['total_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
|
|
if (!$has_amount) {
|
|
continue;
|
|
}
|
|
|
|
if (strpos($account['mapping_type'], '_BASE') !== false) {
|
|
$base_count++;
|
|
} elseif (strpos($account['mapping_type'], '_VAT') !== false) {
|
|
$vat_count++;
|
|
}
|
|
}
|
|
$pdf->Cell(0, 6, 'Comptes de base: ' . $base_count . ' | Comptes de TVA: ' . $vat_count, 0, 1);
|
|
} else {
|
|
// Count non-zero accounts for other lines
|
|
$non_zero_count = 0;
|
|
foreach ($line_details['account_details'] as $account) {
|
|
$has_amount = false;
|
|
if (isset($account['base_amount']) && $account['base_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['vat_amount']) && $account['vat_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['total_amount']) && $account['total_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
|
|
if ($has_amount) {
|
|
$non_zero_count++;
|
|
}
|
|
}
|
|
$pdf->Cell(0, 6, 'Nombre de comptes: ' . $non_zero_count, 0, 1);
|
|
}
|
|
|
|
if (!empty($line_details['calculated_line'])) {
|
|
$calc = $line_details['calculated_line'];
|
|
|
|
// Special handling for lines with both base and VAT amounts (08, 09, 9B)
|
|
if (in_array($line_code, array('08', '09', '9B'))) {
|
|
$pdf->Cell(0, 6, 'Montant de base: ' . price($calc->base_amount, 0, '', 1, 0) . ' | Montant de TVA: ' . price($calc->vat_amount, 0, '', 1, 0), 0, 1);
|
|
} else {
|
|
// Standard VAT amount for other lines
|
|
$pdf->Cell(0, 6, 'Montant calculé: ' . price($calc->vat_amount, 0, '', 1, 0), 0, 1);
|
|
}
|
|
}
|
|
|
|
$pdf->Ln(10);
|
|
|
|
// Add account details table
|
|
$this->addAccountDetailsTable($pdf, $line_details['account_details'], $line_code);
|
|
|
|
}
|
|
|
|
/**
|
|
* Add account details table to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param array $account_details Account details array
|
|
* @param string $line_code CA-3 line code
|
|
*/
|
|
private function addAccountDetailsTable($pdf, $account_details, $line_code)
|
|
{
|
|
// Group accounts by type for lines 08, 09, 9B (same as view page)
|
|
$base_accounts = array();
|
|
$vat_accounts = array();
|
|
$other_accounts = array();
|
|
|
|
foreach ($account_details as $account) {
|
|
// Only include accounts with non-zero amounts to save paper
|
|
$has_amount = false;
|
|
if (isset($account['base_amount']) && $account['base_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['vat_amount']) && $account['vat_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
if (isset($account['total_amount']) && $account['total_amount'] > 0) {
|
|
$has_amount = true;
|
|
}
|
|
|
|
// Skip accounts with zero amounts
|
|
if (!$has_amount) {
|
|
continue;
|
|
}
|
|
|
|
if (strpos($account['mapping_type'], '_BASE') !== false) {
|
|
$base_accounts[] = $account;
|
|
} elseif (strpos($account['mapping_type'], '_VAT') !== false) {
|
|
$vat_accounts[] = $account;
|
|
} else {
|
|
$other_accounts[] = $account;
|
|
}
|
|
}
|
|
|
|
// Calculate table width and center position (matching first page proportions)
|
|
$col_widths = array(20, 60, 40); // Scaled from first page: Code (20), Libellé (60), Montant (40)
|
|
$table_width = array_sum($col_widths); // 120 total width
|
|
$page_width = $pdf->getPageWidth() - $pdf->getMargins()['left'] - $pdf->getMargins()['right'];
|
|
$start_x = ($page_width - $table_width) / 2 + $pdf->getMargins()['left'];
|
|
|
|
// Set X position to center the table
|
|
$pdf->SetX($start_x);
|
|
|
|
// Table header with light gray background (matching first page style)
|
|
$pdf->SetFont('helvetica', 'B', 8);
|
|
$pdf->SetFillColor(240, 240, 240); // Light gray background
|
|
$pdf->Cell($col_widths[0], 8, 'Code compte', 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 8, 'Libellé compte', 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[2], 8, 'Montant', 1, 1, 'C', true);
|
|
|
|
$total_base_base = 0;
|
|
$total_vat_base = 0;
|
|
$total_base_vat = 0;
|
|
$total_vat_vat = 0;
|
|
$total_base_other = 0;
|
|
$total_vat_other = 0;
|
|
|
|
// Display BASE accounts first (if any)
|
|
if (!empty($base_accounts)) {
|
|
$pdf->SetFont('helvetica', 'B', 7);
|
|
$pdf->SetFillColor(245, 245, 245); // Light gray background
|
|
$pdf->SetX($start_x); // Position to match table alignment
|
|
$pdf->Cell($table_width, 6, 'Comptes de base (ventes)', 1, 1, 'C', true);
|
|
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
foreach ($base_accounts as $account) {
|
|
$total_base_base += $account['base_amount'];
|
|
$total_vat_base += $account['vat_amount'];
|
|
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->SetFillColor(255, 255, 255); // White background for data rows
|
|
$pdf->Cell($col_widths[0], 6, $account['account_code'], 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($account['base_amount'], 0, '', 1, 0), 1, 1, 'R', true);
|
|
}
|
|
|
|
// Subtotal for BASE accounts
|
|
$pdf->SetFont('helvetica', 'B', 7);
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFillColor(248, 248, 248); // Light gray for subtotal
|
|
$pdf->Cell($col_widths[0] + $col_widths[1], 6, 'Sous-total comptes de base', 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($total_base_base, 0, '', 1, 0), 1, 1, 'R', true);
|
|
$pdf->Ln(2);
|
|
}
|
|
|
|
// Display VAT accounts second (if any)
|
|
if (!empty($vat_accounts)) {
|
|
$pdf->SetFont('helvetica', 'B', 7);
|
|
$pdf->SetFillColor(245, 245, 245); // Light gray background
|
|
$pdf->SetX($start_x); // Position to match table alignment
|
|
$pdf->Cell($table_width, 6, 'Comptes de TVA', 1, 1, 'C', true);
|
|
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
foreach ($vat_accounts as $account) {
|
|
$total_base_vat += $account['base_amount'];
|
|
$total_vat_vat += $account['vat_amount'];
|
|
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->SetFillColor(255, 255, 255); // White background for data rows
|
|
$pdf->Cell($col_widths[0], 6, $account['account_code'], 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($account['vat_amount'], 0, '', 1, 0), 1, 1, 'R', true);
|
|
}
|
|
|
|
// Subtotal for VAT accounts
|
|
$pdf->SetFont('helvetica', 'B', 7);
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFillColor(248, 248, 248); // Light gray for subtotal
|
|
$pdf->Cell($col_widths[0] + $col_widths[1], 6, 'Sous-total comptes de TVA', 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($total_vat_vat, 0, '', 1, 0), 1, 1, 'R', true);
|
|
$pdf->Ln(2);
|
|
}
|
|
|
|
// Display other accounts (normal lines)
|
|
if (!empty($other_accounts)) {
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
foreach ($other_accounts as $account) {
|
|
$total_base_other += $account['base_amount'];
|
|
$total_vat_other += $account['vat_amount'];
|
|
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->SetFillColor(255, 255, 255); // White background for data rows
|
|
$pdf->Cell($col_widths[0], 6, $account['account_code'], 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($account['vat_amount'], 0, '', 1, 0), 1, 1, 'R', true);
|
|
}
|
|
|
|
// Total for other accounts
|
|
if (!empty($other_accounts)) {
|
|
$pdf->SetFont('helvetica', 'B', 7);
|
|
$pdf->SetX($start_x);
|
|
$pdf->SetFillColor(248, 248, 248); // Light gray for total
|
|
$pdf->Cell($col_widths[0] + $col_widths[1], 6, 'Total', 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, price($total_vat_other, 0, '', 1, 0), 1, 1, 'R', true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add detailed pages to an existing PDF
|
|
*
|
|
* @param string $pdf_path Path to the existing PDF
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function addDetailedPagesToPDF($pdf_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Create a temporary detailed PDF
|
|
$temp_detailed_path = tempnam(sys_get_temp_dir(), 'ca3_detailed_') . '.pdf';
|
|
|
|
// Use the improved layout for detailed pages
|
|
$result = $this->generateImprovedPDF($temp_detailed_path, $declaration, $ca3_data, $mysoc);
|
|
|
|
if (!$result) {
|
|
return false;
|
|
}
|
|
|
|
// Check if detailed PDF was created and has content
|
|
if (!file_exists($temp_detailed_path) || filesize($temp_detailed_path) == 0) {
|
|
return false;
|
|
}
|
|
|
|
// Use a different approach: use pdftk to merge PDFs (preserves form fields)
|
|
$merge_result = $this->mergePDFsWithPdftk($pdf_path, $temp_detailed_path, $pdf_path);
|
|
|
|
if (!$merge_result) {
|
|
// Fallback: try FPDI merge
|
|
$merge_result = $this->mergePDFs($pdf_path, $temp_detailed_path, $pdf_path);
|
|
if (!$merge_result) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Clean up temporary file
|
|
if (file_exists($temp_detailed_path)) {
|
|
unlink($temp_detailed_path);
|
|
}
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to add detailed pages: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge two PDFs using pdftk (preserves form fields)
|
|
*
|
|
* @param string $pdf1_path First PDF path
|
|
* @param string $pdf2_path Second PDF path
|
|
* @param string $output_path Output PDF path
|
|
* @return bool Success
|
|
*/
|
|
private function mergePDFsWithPdftk($pdf1_path, $pdf2_path, $output_path)
|
|
{
|
|
try {
|
|
// Check if pdftk is available
|
|
if (!$this->isPdftkAvailable()) {
|
|
$this->error = 'pdftk is not available for PDF merging';
|
|
return false;
|
|
}
|
|
|
|
// Use pdftk to merge PDFs (preserves form fields)
|
|
// Create a temporary output file to avoid pdftk input/output filename conflict
|
|
$temp_output_path = tempnam(sys_get_temp_dir(), 'ca3_merged_') . '.pdf';
|
|
|
|
$pdftk_path = $this->getPdftkPath();
|
|
$command = "\"$pdftk_path\" \"$pdf1_path\" \"$pdf2_path\" cat output \"$temp_output_path\"";
|
|
$result = shell_exec($command . ' 2>&1');
|
|
|
|
// If pdftk succeeded, copy the merged file to the final output path
|
|
if (file_exists($temp_output_path) && filesize($temp_output_path) > 0) {
|
|
copy($temp_output_path, $output_path);
|
|
unlink($temp_output_path); // Clean up temporary file
|
|
}
|
|
|
|
// Check if output file was created successfully
|
|
if (file_exists($output_path) && filesize($output_path) > 0) {
|
|
return true;
|
|
} else {
|
|
$this->error = 'pdftk failed to merge PDFs: ' . $result;
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to merge PDFs with pdftk: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge two PDFs using FPDI with TCPDF
|
|
*
|
|
* @param string $pdf1_path First PDF path
|
|
* @param string $pdf2_path Second PDF path
|
|
* @param string $output_path Output PDF path
|
|
* @return bool Success
|
|
*/
|
|
private function mergePDFs($pdf1_path, $pdf2_path, $output_path)
|
|
{
|
|
try {
|
|
// Use TCPDF-based FPDI
|
|
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi();
|
|
|
|
// Import pages from first PDF
|
|
$page_count1 = $pdf->setSourceFile($pdf1_path);
|
|
for ($i = 1; $i <= $page_count1; $i++) {
|
|
$pdf->AddPage();
|
|
$pdf->useTemplate($pdf->importPage($i));
|
|
}
|
|
|
|
// Import pages from second PDF
|
|
$page_count2 = $pdf->setSourceFile($pdf2_path);
|
|
for ($i = 1; $i <= $page_count2; $i++) {
|
|
$pdf->AddPage();
|
|
$pdf->useTemplate($pdf->importPage($i));
|
|
}
|
|
|
|
// Output merged PDF
|
|
$pdf->Output($output_path, 'F');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to merge PDFs: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translate status to French
|
|
*
|
|
* @param string $status Status in English
|
|
* @return string Status in French
|
|
*/
|
|
private function translateStatus($status)
|
|
{
|
|
$translations = array(
|
|
'draft' => 'Brouillon',
|
|
'validated' => 'Validé',
|
|
'submitted' => 'Soumis',
|
|
'approved' => 'Approuvé',
|
|
'rejected' => 'Rejeté'
|
|
);
|
|
|
|
return isset($translations[$status]) ? $translations[$status] : ucfirst($status);
|
|
}
|
|
|
|
/**
|
|
* Get mapping type label for display
|
|
*
|
|
* @param string $mapping_type Mapping type (e.g., '08_BASE', '08_VAT')
|
|
* @return string Display label
|
|
*/
|
|
private function getMappingTypeLabel($mapping_type)
|
|
{
|
|
if (strpos($mapping_type, '_BASE') !== false) {
|
|
return 'Base';
|
|
} elseif (strpos($mapping_type, '_VAT') !== false) {
|
|
return 'TVA';
|
|
} else {
|
|
return 'Standard';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Truncate text to fit in table cell
|
|
*
|
|
* @param string $text Text to truncate
|
|
* @param int $max_length Maximum length
|
|
* @return string Truncated text
|
|
*/
|
|
private function truncateText($text, $max_length)
|
|
{
|
|
if (strlen($text) <= $max_length) {
|
|
return $text;
|
|
}
|
|
return substr($text, 0, $max_length - 3) . '...';
|
|
}
|
|
|
|
/**
|
|
* Get detail page number for a line (for linking)
|
|
*
|
|
* @param string $line_code CA-3 line code
|
|
* @return int Page number
|
|
*/
|
|
private function getDetailPageNumber($line_code)
|
|
{
|
|
// This is a simplified approach - in a real implementation,
|
|
// you'd track page numbers as you create them
|
|
return 2; // Start from page 2 (after main form)
|
|
}
|
|
|
|
/**
|
|
* Generate detailed PDF with breakdown pages
|
|
*
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function generateDetailedPDF($output_path, $declaration, $ca3_data, $mysoc)
|
|
{
|
|
try {
|
|
// Create a new PDF document
|
|
$pdf = new DeclarationTVA_CustomPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
|
|
|
// Set company name and declaration period for footer
|
|
$pdf->setCompanyName($mysoc->name);
|
|
$pdf->setDeclarationPeriod(dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'));
|
|
|
|
// Set document information
|
|
$pdf->SetCreator('DeclarationTVA Module');
|
|
$pdf->SetAuthor($mysoc->name);
|
|
$pdf->SetTitle('CA-3 Declaration Details ' . $declaration->declaration_number);
|
|
$pdf->SetSubject('French VAT Declaration - Detailed Breakdown');
|
|
|
|
// Set margins
|
|
$pdf->SetMargins(15, 15, 15);
|
|
$pdf->SetHeaderMargin(5);
|
|
$pdf->SetFooterMargin(10);
|
|
|
|
// Set thin borders for all elements
|
|
$pdf->SetLineWidth(0.1);
|
|
|
|
// Add title page
|
|
$pdf->AddPage();
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->SetFillColor(240, 240, 240); // Light gray background
|
|
$pdf->Cell(0, 10, 'Détails de la Déclaration TVA CA-3', 1, 1, 'C', true);
|
|
$pdf->Ln(10);
|
|
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Numéro de déclaration: ' . $declaration->declaration_number, 0, 1);
|
|
$pdf->Cell(0, 8, 'Période: ' . dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'), 0, 1);
|
|
$pdf->Cell(0, 8, 'Statut: ' . $this->translateStatus($declaration->status), 0, 1);
|
|
$pdf->Ln(10);
|
|
|
|
// Add journal entry table on page 1
|
|
$this->addJournalEntryTable($pdf, $declaration, $ca3_data);
|
|
|
|
// Add bank journal entry table on page 1
|
|
$this->addBankJournalEntryTable($pdf, $declaration, $ca3_data);
|
|
|
|
// Add detailed breakdown pages starting on page 2
|
|
$this->addDetailPages($pdf, $declaration, $ca3_data);
|
|
|
|
// Set total pages for footer after all pages are generated
|
|
$pdf->setTotalPages($pdf->getNumPages());
|
|
|
|
// Output PDF
|
|
$pdf->Output($output_path, 'F');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Detailed PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload custom template
|
|
*
|
|
* @param array $file Uploaded file array
|
|
* @return bool Success
|
|
*/
|
|
public function uploadCustomTemplate($file)
|
|
{
|
|
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
|
$this->error = 'No file uploaded';
|
|
return false;
|
|
}
|
|
|
|
// Validate file type
|
|
$file_info = pathinfo($file['name']);
|
|
if (strtolower($file_info['extension']) !== 'pdf') {
|
|
$this->error = 'Only PDF files are allowed';
|
|
return false;
|
|
}
|
|
|
|
// Validate file size (max 10MB)
|
|
if ($file['size'] > 10 * 1024 * 1024) {
|
|
$this->error = 'File too large (max 10MB)';
|
|
return false;
|
|
}
|
|
|
|
// Ensure template directory exists
|
|
if (!is_dir($this->template_path)) {
|
|
if (!dol_mkdir($this->template_path)) {
|
|
$this->error = 'Failed to create template directory';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check directory permissions
|
|
if (!is_writable($this->template_path)) {
|
|
$this->error = 'Template directory is not writable';
|
|
return false;
|
|
}
|
|
|
|
// Move uploaded file
|
|
$target_path = $this->template_path . 'ca3_custom_template.pdf';
|
|
|
|
// Remove existing custom template if it exists
|
|
if (file_exists($target_path)) {
|
|
unlink($target_path);
|
|
}
|
|
|
|
if (move_uploaded_file($file['tmp_name'], $target_path)) {
|
|
// Verify the file was saved correctly
|
|
if (file_exists($target_path) && filesize($target_path) > 0) {
|
|
return true;
|
|
} else {
|
|
$this->error = 'Template file was not saved correctly';
|
|
return false;
|
|
}
|
|
} else {
|
|
$last_error = error_get_last();
|
|
$this->error = 'Failed to save template: ' . ($last_error ? $last_error['message'] : 'Unknown error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get template information
|
|
*
|
|
* @return array Template information
|
|
*/
|
|
public function getTemplateInfo()
|
|
{
|
|
$info = array(
|
|
'version' => $this->template_version,
|
|
'document' => $this->template_document,
|
|
'official_number' => $this->template_document . '*' . $this->template_version,
|
|
'custom_template' => false,
|
|
'template_path' => ''
|
|
);
|
|
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
$info['custom_template'] = true;
|
|
$info['template_path'] = $custom_template;
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Reset to default template
|
|
*
|
|
* @return bool Success
|
|
*/
|
|
public function resetToDefaultTemplate()
|
|
{
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
return unlink($custom_template);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check for template updates from Gitea
|
|
*
|
|
* @return array Update information
|
|
*/
|
|
public function checkTemplateUpdates()
|
|
{
|
|
$update_info = array(
|
|
'update_available' => false,
|
|
'current_version' => $this->template_version,
|
|
'latest_version' => $this->template_version,
|
|
'download_url' => '',
|
|
'release_date' => '',
|
|
'error' => ''
|
|
);
|
|
|
|
try {
|
|
// Try to fetch manifest from Gitea, fallback to local if not accessible
|
|
$manifest_content = @file_get_contents($this->manifest_url);
|
|
if ($manifest_content === false) {
|
|
// Fallback to local manifest file
|
|
$local_manifest = DOL_DOCUMENT_ROOT.'/custom/declarationtva/templates/manifest.json';
|
|
if (file_exists($local_manifest)) {
|
|
$manifest_content = file_get_contents($local_manifest);
|
|
// Set a note that we're using local manifest
|
|
$update_info['error'] = 'Using local manifest (Gitea server not accessible - repository may be private or server configuration issue)';
|
|
}
|
|
if ($manifest_content === false) {
|
|
$update_info['error'] = 'Failed to fetch manifest from Gitea and local file not found';
|
|
return $update_info;
|
|
}
|
|
}
|
|
|
|
$manifest = json_decode($manifest_content, true);
|
|
if (!$manifest || !isset($manifest['templates']['ca3'])) {
|
|
$update_info['error'] = 'Invalid manifest format';
|
|
return $update_info;
|
|
}
|
|
|
|
$template_info = $manifest['templates']['ca3'];
|
|
$latest_version = $template_info['current_version'];
|
|
$current_version = $this->template_version;
|
|
|
|
// Check if update is available
|
|
if (version_compare($latest_version, $current_version, '>')) {
|
|
$update_info['update_available'] = true;
|
|
$update_info['latest_version'] = $latest_version;
|
|
|
|
if (isset($template_info['releases'][$latest_version])) {
|
|
$release_info = $template_info['releases'][$latest_version];
|
|
$update_info['download_url'] = $release_info['download_url'];
|
|
$update_info['release_date'] = $release_info['release_date'];
|
|
}
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$update_info['error'] = 'Error checking updates: ' . $e->getMessage();
|
|
}
|
|
|
|
return $update_info;
|
|
}
|
|
|
|
/**
|
|
* Download and install template update
|
|
*
|
|
* @param string $version Version to download
|
|
* @param string $download_url Download URL
|
|
* @return bool Success
|
|
*/
|
|
public function downloadTemplateUpdate($version, $download_url)
|
|
{
|
|
try {
|
|
// Ensure template directory exists
|
|
if (!is_dir($this->template_path)) {
|
|
dol_mkdir($this->template_path);
|
|
}
|
|
|
|
// Try to download template from Gitea, fallback to local if not accessible
|
|
$template_content = @file_get_contents($download_url);
|
|
if ($template_content === false) {
|
|
// Fallback to local template file
|
|
$local_template = DOL_DOCUMENT_ROOT.'/custom/declarationtva/templates/declarationtva/ca3_official_template.pdf';
|
|
if (file_exists($local_template)) {
|
|
$template_content = file_get_contents($local_template);
|
|
// Note that we're using local template
|
|
$this->error = 'Using local template (Gitea server not accessible - repository may be private or server configuration issue)';
|
|
}
|
|
if ($template_content === false) {
|
|
$this->error = 'Failed to download template from Gitea and local file not found';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Save as official template
|
|
$template_file = $this->template_path . 'ca3_official_template.pdf';
|
|
if (file_put_contents($template_file, $template_content) === false) {
|
|
$this->error = 'Failed to save downloaded template';
|
|
return false;
|
|
}
|
|
|
|
// Update version info
|
|
$this->template_version = $version;
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Error downloading template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get template update status
|
|
*
|
|
* @return array Status information
|
|
*/
|
|
public function getTemplateUpdateStatus()
|
|
{
|
|
$status = array(
|
|
'current_version' => $this->template_version,
|
|
'update_available' => false,
|
|
'latest_version' => $this->template_version,
|
|
'last_check' => '',
|
|
'error' => ''
|
|
);
|
|
|
|
// Check for updates
|
|
$update_info = $this->checkTemplateUpdates();
|
|
|
|
if ($update_info['update_available']) {
|
|
$status['update_available'] = true;
|
|
$status['latest_version'] = $update_info['latest_version'];
|
|
}
|
|
|
|
if (!empty($update_info['error'])) {
|
|
$status['error'] = $update_info['error'];
|
|
}
|
|
|
|
// Store last check time
|
|
$status['last_check'] = date('Y-m-d H:i:s');
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Auto-update template if available
|
|
*
|
|
* @return bool Success
|
|
*/
|
|
public function autoUpdateTemplate()
|
|
{
|
|
$update_info = $this->checkTemplateUpdates();
|
|
|
|
if ($update_info['update_available'] && !empty($update_info['download_url'])) {
|
|
return $this->downloadTemplateUpdate(
|
|
$update_info['latest_version'],
|
|
$update_info['download_url']
|
|
);
|
|
}
|
|
|
|
return true; // No update needed
|
|
}
|
|
|
|
/**
|
|
* Get CA-3 line amount from the data array
|
|
*
|
|
* @param array $ca3_data CA-3 data array
|
|
* @param string $ca3_line Line identifier (e.g., 'A1', '08', '17')
|
|
* @param string $amount_type Amount type ('vat_amount' or 'base_amount')
|
|
* @return string Formatted amount
|
|
*/
|
|
private function getCA3LineAmount($ca3_data, $ca3_line, $amount_type = 'vat_amount')
|
|
{
|
|
if (empty($ca3_data) || !is_array($ca3_data)) {
|
|
return '0,00';
|
|
}
|
|
|
|
$calculated_amount = null;
|
|
$mapped_amount = null;
|
|
|
|
// Search through the array for the matching ca3_line
|
|
foreach ($ca3_data as $line_data) {
|
|
if (isset($line_data['ca3_line']) && $line_data['ca3_line'] == $ca3_line) {
|
|
$amount = isset($line_data[$amount_type]) ? $line_data[$amount_type] : 0;
|
|
|
|
// Prioritize calculated entries over "No accounts mapped" entries
|
|
if (isset($line_data['line_label']) && strpos($line_data['line_label'], 'Calculated') !== false) {
|
|
$calculated_amount = $amount;
|
|
} elseif (isset($line_data['line_label']) && strpos($line_data['line_label'], 'No accounts mapped') === false) {
|
|
$mapped_amount = $amount;
|
|
} else {
|
|
// This is a "No accounts mapped" entry, only use if no other option
|
|
if ($calculated_amount === null && $mapped_amount === null) {
|
|
$mapped_amount = $amount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return calculated amount if available, otherwise mapped amount, otherwise 0
|
|
$final_amount = $calculated_amount !== null ? $calculated_amount :
|
|
($mapped_amount !== null ? $mapped_amount : 0);
|
|
|
|
return $this->formatAmount($final_amount);
|
|
}
|
|
|
|
/**
|
|
* Format SIRET number with special spacing
|
|
* Adds space between every character, with 3 extra spaces before last 5 characters
|
|
*
|
|
* @param string $siret SIRET number
|
|
* @return string Formatted SIRET
|
|
*/
|
|
private function formatSiret($siret)
|
|
{
|
|
// Remove any existing spaces or special characters
|
|
$siret = preg_replace('/[^0-9]/', '', $siret);
|
|
|
|
// If SIRET is not configured or too short, return as is
|
|
if (empty($siret) || $siret === 'SIRET_NOT_CONFIGURED' || strlen($siret) < 5) {
|
|
return $siret;
|
|
}
|
|
|
|
// Split into parts: main part and last 5 characters
|
|
$main_part = substr($siret, 0, -5);
|
|
$last_5 = substr($siret, -5);
|
|
|
|
// Add space between every character in main part
|
|
$formatted_main = implode(' ', str_split($main_part));
|
|
|
|
// Add space between every character in last 5, with 3 extra spaces before
|
|
$formatted_last = ' ' . implode(' ', str_split($last_5));
|
|
|
|
return $formatted_main . $formatted_last;
|
|
}
|
|
|
|
/**
|
|
* Format VAT number with special spacing
|
|
* Adds space between every character, with 2 extra spaces after first 2 chars and 2 extra spaces after first 4 chars
|
|
*
|
|
* @param string $vat_number VAT number
|
|
* @return string Formatted VAT number
|
|
*/
|
|
private function formatVatNumber($vat_number)
|
|
{
|
|
// Remove any existing spaces or special characters
|
|
$vat_number = preg_replace('/[^A-Z0-9]/', '', strtoupper($vat_number));
|
|
|
|
// If VAT number is not configured or too short, return as is
|
|
if (empty($vat_number) || strlen($vat_number) < 4) {
|
|
return $vat_number;
|
|
}
|
|
|
|
$result = '';
|
|
$length = strlen($vat_number);
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$result .= $vat_number[$i];
|
|
|
|
// Add space between every character
|
|
if ($i < $length - 1) {
|
|
$result .= ' ';
|
|
}
|
|
|
|
// After first 2 characters, add 2 extra spaces
|
|
if ($i == 1) {
|
|
$result .= ' ';
|
|
}
|
|
|
|
// After first 4 characters, add 2 extra spaces
|
|
if ($i == 3) {
|
|
$result .= ' ';
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Add journal entry table to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
*/
|
|
private function addJournalEntryTable($pdf, $declaration, $ca3_data)
|
|
{
|
|
// Add some space before the journal table
|
|
$pdf->Ln(10);
|
|
|
|
// Title with light gray background
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->SetFillColor(240, 240, 240); // Light gray background
|
|
$pdf->Cell(0, 10, 'Écritures du Journal OD', 1, 1, 'C', true);
|
|
$pdf->Ln(10);
|
|
|
|
// Create lookup array for CA-3 data
|
|
$ca3_lookup = array();
|
|
foreach ($ca3_data as $line_data) {
|
|
$ca3_lookup[$line_data['ca3_line']] = $line_data;
|
|
}
|
|
|
|
// Get journal entries
|
|
$journal_entries = $this->generateJournalEntries($declaration, $ca3_lookup);
|
|
|
|
if (empty($journal_entries)) {
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Aucune écriture à générer.', 0, 1);
|
|
return;
|
|
}
|
|
|
|
// Table header
|
|
$pdf->SetFont('helvetica', 'B', 8);
|
|
$pdf->SetFillColor(240, 240, 240);
|
|
|
|
// Column widths - reduced and centered
|
|
$col_widths = array(20, 60, 40, 20, 20);
|
|
$col_headers = array('Code compte', 'Libellé compte', 'Libellé écriture', 'Débit', 'Crédit');
|
|
|
|
// Calculate table width and center position
|
|
$table_width = array_sum($col_widths);
|
|
$page_width = $pdf->getPageWidth() - $pdf->getMargins()['left'] - $pdf->getMargins()['right'];
|
|
$start_x = ($page_width - $table_width) / 2 + $pdf->getMargins()['left'];
|
|
|
|
// Set X position to center the table
|
|
$pdf->SetX($start_x);
|
|
|
|
// Draw header
|
|
for ($i = 0; $i < count($col_headers); $i++) {
|
|
$pdf->Cell($col_widths[$i], 8, $col_headers[$i], 1, 0, 'C', true);
|
|
}
|
|
$pdf->Ln();
|
|
|
|
// Table content
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
|
|
foreach ($journal_entries as $entry) {
|
|
// Set X position to center the table for each row
|
|
$pdf->SetX($start_x);
|
|
|
|
$pdf->Cell($col_widths[0], 6, $entry['account_code'], 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 6, $entry['account_label'], 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, $entry['entry_label'], 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[3], 6, $entry['debit'], 1, 0, 'R', true);
|
|
$pdf->Cell($col_widths[4], 6, $entry['credit'], 1, 1, 'R', true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add bank journal entry table to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
*/
|
|
private function addBankJournalEntryTable($pdf, $declaration, $ca3_data)
|
|
{
|
|
// Add some space before the bank journal table
|
|
$pdf->Ln(15);
|
|
|
|
// Title with light gray background
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->SetFillColor(240, 240, 240); // Light gray background
|
|
$pdf->Cell(0, 10, 'Écritures du Journal de Banque', 1, 1, 'C', true);
|
|
$pdf->Ln(10);
|
|
|
|
// Get bank journal entries
|
|
$bank_entries = $this->generateBankJournalEntries($declaration, $ca3_data);
|
|
|
|
if (empty($bank_entries)) {
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Aucune écriture bancaire à générer.', 0, 1);
|
|
return;
|
|
}
|
|
|
|
// Table header
|
|
$pdf->SetFont('helvetica', 'B', 8);
|
|
$pdf->SetFillColor(240, 240, 240);
|
|
|
|
// Column widths - reduced and centered
|
|
$col_widths = array(20, 60, 40, 20, 20);
|
|
$col_headers = array('Code compte', 'Libellé compte', 'Libellé écriture', 'Débit', 'Crédit');
|
|
|
|
// Calculate table width and center position
|
|
$table_width = array_sum($col_widths);
|
|
$page_width = $pdf->getPageWidth() - $pdf->getMargins()['left'] - $pdf->getMargins()['right'];
|
|
$start_x = ($page_width - $table_width) / 2 + $pdf->getMargins()['left'];
|
|
|
|
// Set X position to center the table
|
|
$pdf->SetX($start_x);
|
|
|
|
// Draw header
|
|
for ($i = 0; $i < count($col_headers); $i++) {
|
|
$pdf->Cell($col_widths[$i], 8, $col_headers[$i], 1, 0, 'C', true);
|
|
}
|
|
$pdf->Ln();
|
|
|
|
// Draw entries
|
|
$pdf->SetFont('helvetica', '', 7);
|
|
$pdf->SetFillColor(255, 255, 255);
|
|
|
|
foreach ($bank_entries as $entry) {
|
|
// Set X position to center the table for each row
|
|
$pdf->SetX($start_x);
|
|
|
|
$pdf->Cell($col_widths[0], 6, $entry['account_code'], 1, 0, 'C', true);
|
|
$pdf->Cell($col_widths[1], 6, $entry['account_label'], 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[2], 6, $entry['entry_label'], 1, 0, 'L', true);
|
|
$pdf->Cell($col_widths[3], 6, $entry['debit'], 1, 0, 'R', true);
|
|
$pdf->Cell($col_widths[4], 6, $entry['credit'], 1, 1, 'R', true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate bank journal entries based on CA-3 data
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @return array Bank journal entries
|
|
*/
|
|
private function generateBankJournalEntries($declaration, $ca3_data)
|
|
{
|
|
$entries = array();
|
|
|
|
// Get bank account configuration
|
|
$bank_config = $this->getBankAccountConfiguration();
|
|
$bank_account_id = $bank_config['bank_account'];
|
|
|
|
if (empty($bank_account_id) || $bank_account_id == 0) {
|
|
// No bank account configured
|
|
return $entries;
|
|
}
|
|
|
|
// Get bank account details
|
|
$bank_account = $this->getBankAccountDetails($bank_account_id);
|
|
if (!$bank_account) {
|
|
return $entries;
|
|
}
|
|
|
|
// Calculate VAT amounts the same way as in getBalancingEntries
|
|
$line_16_amount = $this->getLineAmount($declaration, '16');
|
|
$line_23_amount = $this->getLineAmount($declaration, '23');
|
|
$td_amount = $line_16_amount - $line_23_amount;
|
|
|
|
// Determine net VAT due or VAT credit
|
|
$net_vat_due = 0;
|
|
$vat_credit = 0;
|
|
|
|
if ($td_amount > 0) {
|
|
$net_vat_due = $td_amount;
|
|
} elseif ($td_amount < 0) {
|
|
$vat_credit = abs($td_amount);
|
|
}
|
|
|
|
// Get journal configuration for VAT accounts
|
|
$journal_config = $this->getJournalConfiguration();
|
|
|
|
if ($net_vat_due > 0) {
|
|
// VAT payment case - money going out
|
|
// 1. Bank account (credit - money leaving)
|
|
$entries[] = array(
|
|
'account_code' => $bank_account['account_code'],
|
|
'account_label' => $bank_account['account_label'],
|
|
'entry_label' => 'Paiement TVA - ' . $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => $this->formatAmountReal($net_vat_due)
|
|
);
|
|
|
|
// 2. VAT account (debit - VAT being paid)
|
|
$entries[] = array(
|
|
'account_code' => $journal_config['vat_to_pay'],
|
|
'account_label' => $this->getAccountLabel($journal_config['vat_to_pay']),
|
|
'entry_label' => 'Paiement TVA - ' . $declaration->declaration_name,
|
|
'debit' => $this->formatAmountReal($net_vat_due),
|
|
'credit' => ''
|
|
);
|
|
|
|
} elseif ($vat_credit > 0) {
|
|
// VAT refund case - money coming in
|
|
// 1. Bank account (debit - money coming in)
|
|
$entries[] = array(
|
|
'account_code' => $bank_account['account_code'],
|
|
'account_label' => $bank_account['account_label'],
|
|
'entry_label' => 'Remboursement TVA - ' . $declaration->declaration_name,
|
|
'debit' => $this->formatAmountReal($vat_credit),
|
|
'credit' => ''
|
|
);
|
|
|
|
// 2. VAT account (credit - VAT being received)
|
|
$entries[] = array(
|
|
'account_code' => $journal_config['vat_to_receive'],
|
|
'account_label' => $this->getAccountLabel($journal_config['vat_to_receive']),
|
|
'entry_label' => 'Remboursement TVA - ' . $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => $this->formatAmountReal($vat_credit)
|
|
);
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Get bank account configuration
|
|
*
|
|
* @return array Bank account configuration
|
|
*/
|
|
private function getBankAccountConfiguration()
|
|
{
|
|
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
|
|
$config = new DeclarationTVA_Config($this->db, $this->entity);
|
|
|
|
$bank_config = $config->getBankAccountConfiguration();
|
|
|
|
return $bank_config;
|
|
}
|
|
|
|
/**
|
|
* Get journal configuration
|
|
*
|
|
* @return array Journal configuration
|
|
*/
|
|
private function getJournalConfiguration()
|
|
{
|
|
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
|
|
$config = new DeclarationTVA_Config($this->db, $this->entity);
|
|
return $config->getJournalConfiguration();
|
|
}
|
|
|
|
/**
|
|
* Get bank account details
|
|
*
|
|
* @param int $bank_account_id Bank account ID
|
|
* @return array|false Bank account details or false if not found
|
|
*/
|
|
private function getBankAccountDetails($bank_account_id)
|
|
{
|
|
// Get bank account info and its linked accounting account
|
|
$sql = "SELECT ba.rowid, ba.label, ba.number, ba.bank, ba.account_number
|
|
FROM " . MAIN_DB_PREFIX . "bank_account ba
|
|
WHERE ba.rowid = " . (int)$bank_account_id . "
|
|
AND ba.entity = " . $this->entity;
|
|
|
|
$result = $this->db->query($sql);
|
|
if ($result && $this->db->num_rows($result) > 0) {
|
|
$obj = $this->db->fetch_object($result);
|
|
|
|
// Get the accounting account label for the account code
|
|
$account_label = $this->getAccountLabel($obj->account_number);
|
|
|
|
return array(
|
|
'rowid' => $obj->rowid,
|
|
'label' => $obj->label,
|
|
'number' => $obj->number,
|
|
'bank' => $obj->bank,
|
|
'account_code' => $obj->account_number, // Use the linked accounting account code
|
|
'account_label' => $account_label
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Generate journal entries based on CA-3 data
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_lookup CA-3 data lookup array
|
|
* @return array Journal entries
|
|
*/
|
|
private function generateJournalEntries($declaration, $ca3_lookup)
|
|
{
|
|
$entries = array();
|
|
|
|
// Get line 8 VAT accounts (debit side, non-zero only)
|
|
$line8_entries = $this->getLine8VATAccounts($declaration, $ca3_lookup);
|
|
$entries = array_merge($entries, $line8_entries);
|
|
|
|
// Get line 20 accounts (credit side, non-zero only)
|
|
$line20_entries = $this->getLine20Accounts($declaration, $ca3_lookup);
|
|
$entries = array_merge($entries, $line20_entries);
|
|
|
|
// Add balancing entry to ensure debits equal credits
|
|
$balancing_entries = $this->getBalancingEntries($declaration, $entries);
|
|
foreach ($balancing_entries as $entry) {
|
|
if ($entry) {
|
|
$entries[] = $entry;
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Get line 8 VAT accounts for debit side
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_lookup CA-3 data lookup array
|
|
* @return array Journal entries
|
|
*/
|
|
private function getLine8VATAccounts($declaration, $ca3_lookup)
|
|
{
|
|
$entries = array();
|
|
|
|
if (!isset($ca3_lookup['08'])) {
|
|
return $entries;
|
|
}
|
|
|
|
$line8_data = $ca3_lookup['08'];
|
|
$line8_details = $declaration->getCA3LineDetails($declaration->rowid, '08');
|
|
|
|
if (empty($line8_details) || empty($line8_details['account_details'])) {
|
|
return $entries;
|
|
}
|
|
|
|
foreach ($line8_details['account_details'] as $account) {
|
|
// Only include 445 accounts (VAT accounts) with non-zero amounts
|
|
if (strpos($account['account_code'], '445') === 0 && $account['vat_amount'] > 0) {
|
|
$entries[] = array(
|
|
'account_code' => $account['account_code'],
|
|
'account_label' => $account['account_label'],
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => $this->formatAmountReal($account['vat_amount']),
|
|
'credit' => ''
|
|
);
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Get line 20 accounts for credit side
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_lookup CA-3 data lookup array
|
|
* @return array Journal entries
|
|
*/
|
|
private function getLine20Accounts($declaration, $ca3_lookup)
|
|
{
|
|
$entries = array();
|
|
|
|
if (!isset($ca3_lookup['20'])) {
|
|
return $entries;
|
|
}
|
|
|
|
$line20_data = $ca3_lookup['20'];
|
|
$line20_details = $declaration->getCA3LineDetails($declaration->rowid, '20');
|
|
|
|
if (empty($line20_details) || empty($line20_details['account_details'])) {
|
|
return $entries;
|
|
}
|
|
|
|
foreach ($line20_details['account_details'] as $account) {
|
|
// Only include 445 accounts (VAT accounts) with non-zero amounts
|
|
if (strpos($account['account_code'], '445') === 0 && $account['vat_amount'] > 0) {
|
|
$entries[] = array(
|
|
'account_code' => $account['account_code'],
|
|
'account_label' => $account['account_label'],
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => $this->formatAmountReal($account['vat_amount'])
|
|
);
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Get VAT result entry on account 4455100 or 4456700
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_lookup CA-3 data lookup array
|
|
* @return array|null Journal entry
|
|
*/
|
|
private function getVATResultEntry($declaration, $ca3_lookup)
|
|
{
|
|
// Get TD line amount (Line 16 - Line 23)
|
|
$line_16_amount = $this->getLineAmount($declaration, '16');
|
|
$line_23_amount = $this->getLineAmount($declaration, '23');
|
|
$td_amount = $line_16_amount - $line_23_amount;
|
|
|
|
// If TD = 0, use line 27 value and account 4456700
|
|
if (abs($td_amount) < 0.01) {
|
|
$line27_amount = $this->getLineAmount($declaration, '27');
|
|
|
|
if (abs($line27_amount) < 0.01) {
|
|
return null;
|
|
}
|
|
|
|
$entry = array(
|
|
'account_code' => '4456700',
|
|
'account_label' => $this->getAccountLabel('4456700'),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => ''
|
|
);
|
|
|
|
// Put line 23 amount on debit side
|
|
$line23_amount = $this->getLineAmount($declaration, '23');
|
|
if ($line23_amount > 0) {
|
|
$entry['debit'] = $this->formatAmount($line23_amount);
|
|
}
|
|
|
|
return $entry;
|
|
}
|
|
|
|
// If TD > 0, use TD value and account 4455100 (existing logic)
|
|
$entry = array(
|
|
'account_code' => '4455100',
|
|
'account_label' => $this->getAccountLabel('4455100'),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => ''
|
|
);
|
|
|
|
if ($td_amount < 0) {
|
|
$entry['debit'] = $this->formatAmount(abs($td_amount));
|
|
} else {
|
|
$entry['credit'] = $this->formatAmount($td_amount);
|
|
}
|
|
|
|
return $entry;
|
|
}
|
|
|
|
/**
|
|
* Get rounding difference entry
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_lookup CA-3 data lookup array
|
|
* @return array|null Journal entry
|
|
*/
|
|
private function getRoundingEntry($declaration, $ca3_lookup)
|
|
{
|
|
// Calculate rounding difference
|
|
$total_vat_due = 0;
|
|
$total_vat_deductible = 0;
|
|
|
|
// Sum all VAT due amounts
|
|
foreach ($ca3_lookup as $line_data) {
|
|
if (in_array($line_data['ca3_line'], array('08', '09', '9B', '17'))) {
|
|
$total_vat_due += $line_data['vat_amount'];
|
|
}
|
|
}
|
|
|
|
// Sum all VAT deductible amounts
|
|
foreach ($ca3_lookup as $line_data) {
|
|
if (in_array($line_data['ca3_line'], array('19', '20', '21', '22'))) {
|
|
$total_vat_deductible += $line_data['vat_amount'];
|
|
}
|
|
}
|
|
|
|
$vat_result = $total_vat_due - $total_vat_deductible;
|
|
$rounded_result = round($vat_result);
|
|
$rounding_diff = $vat_result - $rounded_result;
|
|
|
|
if (abs($rounding_diff) < 0.01) {
|
|
return null; // No rounding difference
|
|
}
|
|
|
|
$entry = array(
|
|
'account_code' => '',
|
|
'account_label' => '',
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => ''
|
|
);
|
|
|
|
if ($rounding_diff < 0) {
|
|
$entry['account_code'] = '658000';
|
|
$entry['account_label'] = 'Charges exceptionnelles';
|
|
$entry['debit'] = $this->formatAmountReal(abs($rounding_diff));
|
|
} else {
|
|
$entry['account_code'] = '758000';
|
|
$entry['account_label'] = 'Produits exceptionnels';
|
|
$entry['credit'] = $this->formatAmountReal($rounding_diff);
|
|
}
|
|
|
|
return $entry;
|
|
}
|
|
|
|
/**
|
|
* Get balancing entries (main balancing + rounding)
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $entries Current journal entries
|
|
* @return array Array of balancing entries
|
|
*/
|
|
private function getBalancingEntries($declaration, $entries)
|
|
{
|
|
$total_debits = 0;
|
|
$total_credits = 0;
|
|
|
|
// Calculate totals from existing entries
|
|
foreach ($entries as $entry) {
|
|
if (!empty($entry['debit'])) {
|
|
$total_debits += $this->parseAmount($entry['debit']);
|
|
}
|
|
if (!empty($entry['credit'])) {
|
|
$total_credits += $this->parseAmount($entry['credit']);
|
|
}
|
|
}
|
|
|
|
$difference = $total_debits - $total_credits;
|
|
|
|
// If difference is very small (less than 0.01), no balancing entry needed
|
|
if (abs($difference) < 0.01) {
|
|
return array();
|
|
}
|
|
|
|
$balancing_entries = array();
|
|
|
|
// Get journal configuration
|
|
$journal_config = $this->getJournalConfiguration();
|
|
|
|
// Use TD line calculation (Line 16 - Line 23) as main balancing amount
|
|
$line_16_amount = $this->getLineAmount($declaration, '16');
|
|
$line_23_amount = $this->getLineAmount($declaration, '23');
|
|
$td_amount = $line_16_amount - $line_23_amount;
|
|
|
|
// If TD <= 0, use line 26 or 27 (whichever is not 0) and configured VAT to receive account on debit side
|
|
if ($td_amount <= 0) {
|
|
$line26_amount = $this->getLineAmount($declaration, '26');
|
|
$line27_amount = $this->getLineAmount($declaration, '27');
|
|
|
|
// Use whichever line is not 0
|
|
$selected_amount = 0;
|
|
if (abs($line26_amount) >= 0.01) {
|
|
$selected_amount = $line26_amount;
|
|
} elseif (abs($line27_amount) >= 0.01) {
|
|
$selected_amount = $line27_amount;
|
|
}
|
|
|
|
if (abs($selected_amount) >= 0.01) {
|
|
$balancing_entries[] = array(
|
|
'account_code' => $journal_config['vat_to_receive'],
|
|
'account_label' => $this->getAccountLabel($journal_config['vat_to_receive']),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => $this->formatAmount($selected_amount),
|
|
'credit' => ''
|
|
);
|
|
}
|
|
} else {
|
|
// If TD > 0, use TD value and configured VAT to pay account
|
|
$balancing_entries[] = array(
|
|
'account_code' => $journal_config['vat_to_pay'],
|
|
'account_label' => $this->getAccountLabel($journal_config['vat_to_pay']),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => $this->formatAmount(abs($td_amount))
|
|
);
|
|
}
|
|
|
|
// Add final balancing entry to ensure debits equal credits
|
|
$final_debits = 0;
|
|
$final_credits = 0;
|
|
|
|
// Calculate totals including the main balancing entries we just added
|
|
foreach ($balancing_entries as $entry) {
|
|
if (!empty($entry['debit'])) {
|
|
$final_debits += $this->parseAmount($entry['debit']);
|
|
}
|
|
if (!empty($entry['credit'])) {
|
|
$final_credits += $this->parseAmount($entry['credit']);
|
|
}
|
|
}
|
|
|
|
// Add the existing entries to the calculation
|
|
foreach ($entries as $entry) {
|
|
if (!empty($entry['debit'])) {
|
|
$final_debits += $this->parseAmount($entry['debit']);
|
|
}
|
|
if (!empty($entry['credit'])) {
|
|
$final_credits += $this->parseAmount($entry['credit']);
|
|
}
|
|
}
|
|
|
|
$final_difference = $final_debits - $final_credits;
|
|
|
|
// If there's still a difference, add final balancing entry
|
|
if (abs($final_difference) >= 0.01) {
|
|
if ($final_difference > 0) {
|
|
// More debits, need credit entry on other products account
|
|
$balancing_entries[] = array(
|
|
'account_code' => $journal_config['other_products'],
|
|
'account_label' => $this->getAccountLabel($journal_config['other_products']),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => '',
|
|
'credit' => $this->formatAmountReal($final_difference)
|
|
);
|
|
} else {
|
|
// More credits, need debit entry on other charges account
|
|
$balancing_entries[] = array(
|
|
'account_code' => $journal_config['other_charges'],
|
|
'account_label' => $this->getAccountLabel($journal_config['other_charges']),
|
|
'entry_label' => $declaration->declaration_name,
|
|
'debit' => $this->formatAmountReal(abs($final_difference)),
|
|
'credit' => ''
|
|
);
|
|
}
|
|
}
|
|
|
|
return $balancing_entries;
|
|
}
|
|
|
|
/**
|
|
* Get line amount for a specific CA-3 line
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param string $line_code CA-3 line code
|
|
* @return float Line amount
|
|
*/
|
|
private function getLineAmount($declaration, $line_code)
|
|
{
|
|
$sql = "SELECT vat_amount FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
|
|
WHERE declaration_id = " . $declaration->rowid . "
|
|
AND ca3_line = '" . $this->db->escape($line_code) . "'";
|
|
|
|
$result = $this->db->query($sql);
|
|
if ($result && $this->db->num_rows($result) > 0) {
|
|
$obj = $this->db->fetch_object($result);
|
|
return (float)$obj->vat_amount;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get VAT amount from line TD
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @return float VAT amount from line TD
|
|
*/
|
|
private function getVATAmountFromLineTD($declaration)
|
|
{
|
|
$sql = "SELECT vat_amount FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
|
|
WHERE declaration_id = " . $declaration->rowid . "
|
|
AND ca3_line = 'TD'
|
|
AND entity = " . $this->entity;
|
|
|
|
$result = $this->db->query($sql);
|
|
if ($result && $this->db->num_rows($result) > 0) {
|
|
$obj = $this->db->fetch_object($result);
|
|
return (float)$obj->vat_amount;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Parse amount string back to float
|
|
*
|
|
* @param string $amount_string Formatted amount string
|
|
* @return float Parsed amount
|
|
*/
|
|
private function parseAmount($amount_string)
|
|
{
|
|
// Remove spaces and convert comma to dot
|
|
$amount_string = str_replace(' ', '', $amount_string);
|
|
$amount_string = str_replace(',', '.', $amount_string);
|
|
return (float)$amount_string;
|
|
}
|
|
|
|
/**
|
|
* Get account label from chart of accounts
|
|
*
|
|
* @param string $account_code Account code
|
|
* @return string Account label
|
|
*/
|
|
private function getAccountLabel($account_code)
|
|
{
|
|
// Use the same approach as getAccountMappings() in the main class
|
|
// Order by rowid ASC to get the oldest entry (custom uppercase labels)
|
|
$sql = "SELECT a.label
|
|
FROM " . MAIN_DB_PREFIX . "accounting_account a
|
|
WHERE a.account_number = '" . $this->db->escape($account_code) . "'
|
|
AND a.entity = " . $this->entity . "
|
|
ORDER BY a.rowid ASC
|
|
LIMIT 1";
|
|
|
|
$result = $this->db->query($sql);
|
|
if ($result && $this->db->num_rows($result) > 0) {
|
|
$obj = $this->db->fetch_object($result);
|
|
return $obj->label;
|
|
}
|
|
|
|
// If not found with entity, try without entity filter
|
|
$sql = "SELECT a.label
|
|
FROM " . MAIN_DB_PREFIX . "accounting_account a
|
|
WHERE a.account_number = '" . $this->db->escape($account_code) . "'
|
|
ORDER BY a.rowid ASC
|
|
LIMIT 1";
|
|
|
|
$result = $this->db->query($sql);
|
|
if ($result && $this->db->num_rows($result) > 0) {
|
|
$obj = $this->db->fetch_object($result);
|
|
return $obj->label;
|
|
}
|
|
|
|
// If account not found in chart of accounts, return generic label
|
|
return 'Compte ' . $account_code;
|
|
}
|
|
|
|
|
|
}
|