. */ /** * \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; // Both export and validation use the EXACT SAME method // Use the same logic as the original export method $template_file = $this->getTemplatePath(); if (!$template_file) { $this->error = 'CA-3 template not found'; return false; } // Use the same fillPDFTemplate method that export uses $result = $this->fillPDFTemplate($template_file, $output_path, $declaration, $ca3_data, $mysoc); 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; // Use the exact same method as export but with the correct output path // First generate the PDF using the same method as export $temp_pdf = $this->generateCA3PDF($declaration_id, $outputlangs); if ($temp_pdf && file_exists($temp_pdf)) { // Copy the generated PDF to the validation location if (copy($temp_pdf, $filepath)) { // Clean up the temporary file unlink($temp_pdf); return $filepath; } } 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, $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 { // No fillable template available - show error $this->error = 'No fillable PDF template available. Please upload a CA-3 template.'; return false; } } 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 */ /** * 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 */ /** * 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 */ /** * Add detailed pages content to PDF * * @param TCPDF $pdf PDF object * @param DeclarationTVA $declaration Declaration object * @param array $ca3_data CA-3 line data */ private function addDetailedPagesContent($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->addLineDetailPageContent($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 addLineDetailPageContent($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); } /** * Generate improved detailed PDF with new layout * * @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 generateImprovedDetailedPDF($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 Details'); // 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->addDetailedPagesContent($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 detailed PDF generation failed: ' . $e->getMessage(); return false; } } /** * 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'; $result = $this->generateImprovedDetailedPDF($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) } /** * 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; } }