DeclarationTVA/core/class/declarationtva_pdf.class.php
Frank Cools 909a309fe1 Format PDF numbers without decimals
- Updated formatAmount() method to use 0 decimal places instead of 2
- All amounts in PDF will now display as whole numbers (e.g., 1 958 instead of 1 958,00)
- Maintains French number formatting with comma as decimal separator and space as thousands separator
2025-10-03 13:37:57 +02:00

1083 lines
42 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';
/**
* 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
*/
public $template_version = '30';
/**
* @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)
{
$this->db = $db;
$this->entity = (int) $conf->entity;
$this->template_path = DOL_DOCUMENT_ROOT.'/custom/declarationtva/templates/declarationtva/';
}
/**
* Generate CA-3 declaration PDF
*
* @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 = '')
{
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
$company = new Societe($this->db);
$company->fetch($conf->entity);
// Generate PDF filename
$filename = 'CA3_' . $declaration->declaration_number . '_' . 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/');
}
// Check if we have a custom template
$template_file = $this->getTemplatePath();
if (!$template_file) {
$this->error = 'CA-3 template not found';
return false;
}
// Generate PDF using FPDI or similar library
$result = $this->fillPDFTemplate($template_file, $filepath, $declaration, $ca3_data, $company);
if ($result) {
return $filepath;
} else {
return false;
}
}
/**
* 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, $company)
{
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, $company);
} else {
// Fall back to basic PDF generation
return $this->generateBasicPDF($output_path, $declaration, $ca3_data, $company);
}
} 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)
{
// Simple check - if it's our custom template, assume it's fillable
return strpos($pdf_path, 'ca3_custom_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, $company)
{
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, $company);
}
// Prepare field data mapping
$field_data = $this->prepareFieldData($declaration, $ca3_data, $company);
// 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) {
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, $company)
{
$field_data = array();
// Get the actual company information from Dolibarr configuration
global $conf, $mysoc;
// 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;
// Debug: Log company information to see what's available
error_log("DeclarationTVA: Company data - name: " . $mysoc->name);
error_log("DeclarationTVA: Company data - idprof1: " . $mysoc->idprof1);
error_log("DeclarationTVA: Company data - idprof2: " . $mysoc->idprof2);
error_log("DeclarationTVA: Company data - idprof3: " . $mysoc->idprof3);
error_log("DeclarationTVA: Company data - idprof4: " . $mysoc->idprof4);
error_log("DeclarationTVA: Company data - tva_intra: " . $mysoc->tva_intra);
// 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 for testing
if (empty($siret_value)) {
$siret_value = 'SIRET_NOT_CONFIGURED';
error_log("DeclarationTVA: No SIRET found in company configuration. Please configure SIRET in Dolibarr company settings.");
}
$field_data['company_siret'] = $siret_value;
$field_data['company_vat_number'] = $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');
// Debug: Log the CA-3 data structure for troubleshooting
if (empty($ca3_data)) {
error_log("DeclarationTVA: CA-3 data is empty");
// Use test data for debugging
$ca3_data = array(
'A1' => array('vat_amount' => 1000.00),
'A2' => array('vat_amount' => 2000.00),
'08' => array('base_amount' => 5000.00, 'vat_amount' => 1000.00),
'09' => array('base_amount' => 10000.00, 'vat_amount' => 2000.00),
'20' => array('vat_amount' => 500.00),
'F1' => array('vat_amount' => 300.00),
'F2' => array('vat_amount' => 400.00)
);
error_log("DeclarationTVA: Using test data for debugging");
} else {
error_log("DeclarationTVA: CA-3 data structure: " . print_r($ca3_data, true));
}
// Debug: Log specific amount field values
error_log("DeclarationTVA: A1_amount will be: " . $this->getCA3LineAmount($ca3_data, 'A1'));
error_log("DeclarationTVA: B08_vat_amount will be: " . $this->getCA3LineAmount($ca3_data, '08', 'vat_amount'));
error_log("DeclarationTVA: B09_vat_amount will be: " . $this->getCA3LineAmount($ca3_data, '09', 'vat_amount'));
error_log("DeclarationTVA: total_vat_collected will be: " . $this->formatAmount($declaration->total_vat_collected));
// 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
$field_data['F1_amount'] = isset($ca3_data['F1']) ? $this->formatAmount($ca3_data['F1']['vat_amount']) : '0,00';
$field_data['F2_amount'] = isset($ca3_data['F2']) ? $this->formatAmount($ca3_data['F2']['vat_amount']) : '0,00';
// Section D: Résultat (Calculated)
$field_data['D25_amount'] = isset($ca3_data['25']) ? $this->formatAmount($ca3_data['25']['vat_amount']) : '0,00';
$field_data['D26_amount'] = isset($ca3_data['26']) ? $this->formatAmount($ca3_data['26']['vat_amount']) : '0,00';
$field_data['DTD_amount'] = isset($ca3_data['TD']) ? $this->formatAmount($ca3_data['TD']['vat_amount']) : '0,00';
$field_data['D28_amount'] = isset($ca3_data['28']) ? $this->formatAmount($ca3_data['28']['vat_amount']) : '0,00';
$field_data['D29_amount'] = isset($ca3_data['29']) ? $this->formatAmount($ca3_data['29']['vat_amount']) : '0,00';
// Subtotals
$field_data['subtotal_B16_amount'] = isset($ca3_data['16']) ? $this->formatAmount($ca3_data['16']['vat_amount']) : '0,00';
$field_data['subtotal_C23_amount'] = isset($ca3_data['23']) ? $this->formatAmount($ca3_data['23']['vat_amount']) : '0,00';
// 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, ',', ' ');
}
/**
* 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, $company)
{
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, $company);
$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);
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, $company)
{
try {
// Create a new PDF document
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// Set document information
$pdf->SetCreator('DeclarationTVA Module');
$pdf->SetAuthor($company->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);
// 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: ' . $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', 'TD', '28', '29'));
// 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);
}
// 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;
$pdf->Cell(20, 6, $line, 1, 0, 'C');
$pdf->Cell(100, 6, $data['line_label'], 1, 0);
$pdf->Cell(30, 6, price($amount, 0, '', 1, 0), 1, 1, 'R');
}
}
$pdf->Ln(5);
}
/**
* 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 {
// Fetch manifest from Gitea
$manifest_content = file_get_contents($this->manifest_url);
if ($manifest_content === false) {
$update_info['error'] = 'Failed to fetch manifest from Gitea';
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);
}
// Download template
$template_content = file_get_contents($download_url);
if ($template_content === false) {
$this->error = 'Failed to download template from Gitea';
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';
}
// 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;
return $this->formatAmount($amount);
}
}
return '0,00';
}
}