. */ /** * \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'] = $this->formatSiret($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 (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['DTD_amount'] = $this->getCA3LineAmount($ca3_data, 'TD'); $field_data['D28_amount'] = $this->getCA3LineAmount($ca3_data, '28'); $field_data['D29_amount'] = $this->getCA3LineAmount($ca3_data, '29'); // 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, ',', ' '); } /** * 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'; } /** * 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; } }