diff --git a/ChangeLog.md b/ChangeLog.md index 40ae455..7a7370c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,27 @@ # CHANGELOG MODULE DECLARATIONTVA FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) +## 1.1 + +### New Features +- **Enhanced PDF Export**: CA-3 form now includes detailed breakdown pages in a single comprehensive PDF +- **Detailed Breakdown Pages**: Account-level details for each CA-3 line showing exactly where amounts come from +- **Improved PDF Merging**: Uses pdftk for reliable PDF merging that preserves form fields +- **Conditional Page Breaks**: Optimized page usage to reduce paper consumption +- **Enhanced Line Calculations**: Added support for lines 25, 26, 27, TD, 28, 32 with proper conditional visibility +- **Status Translation**: Proper French translation of declaration status in detailed PDFs + +### Technical Improvements +- **Better Error Handling**: Comprehensive logging and debugging for PDF generation +- **Form Field Preservation**: pdftk-based merging maintains all form data integrity +- **Modular PDF Generation**: Separate detailed PDF generation with fallback options +- **Enhanced Debugging**: Detailed logging for troubleshooting PDF generation issues + +### Bug Fixes +- **Fixed Multi-Select Saving**: Configuration fields now properly save when all selections are removed +- **Fixed PDF Merge Issues**: Resolved pdftk filename conflicts that prevented detailed pages from being added +- **Fixed Status Translation**: Eliminated "Call to a member function trans() on null" errors +- **Fixed Form Corruption**: Prevented FPDI merge from corrupting filled form data + ## 1.0 Initial version diff --git a/TASK.md b/TASK.md index 08d30f0..a3c685f 100644 --- a/TASK.md +++ b/TASK.md @@ -712,7 +712,143 @@ The MVP is now **feature-complete** and ready for real-world French VAT declarat ## Discovered During Work -*Tasks discovered during development will be added here* +### Bug Fixes - Configuration Screen +- [x] **Task Bug-1**: Fix multiple selection fields not saving when all selections are removed ✅ **COMPLETED** + - **Issue**: When removing all selections from multi-select fields in config screen, changes were not persisted + - **Root Cause**: Form processing logic only called `updateAccountMapping` when `isset($_POST['field_name'])` was true, but empty multi-select fields are not present in POST data + - **Solution**: Modified form processing to always call `updateAccountMapping` for each field, regardless of POST data presence + - **Files Modified**: `admin/setup_mvp.php`, `core/class/declarationtva_config.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 2 hours + +### Bug Fixes - CA-3 Line 25 Calculation +- [x] **Task Bug-2**: Fix line 25 calculation and title ✅ **COMPLETED** + - **Issue**: Line 25 had incorrect title and calculation logic + - **Requirements**: Title should be 'Crédit de TVA (ligne 23 - ligne16)', calculation should be line 23 - line 16, only display if > 0 + - **Root Cause**: Line 25 was using incorrect calculation logic based on total VAT collected instead of line 23 - line 16 + - **Solution**: + - Added `getLineAmount()` helper method to retrieve specific line amounts + - Modified `calculateDSectionLines()` to calculate line 25 as line 23 - line 16 + - Updated title to 'Crédit de TVA (ligne 23 - ligne16)' + - Only display value if > 0 + - **Files Modified**: `core/class/declarationtva.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 1 hour + +### Bug Fixes - CA-3 Line 28 Calculation +- [x] **Task Bug-3**: Fix line 28 calculation and title ✅ **COMPLETED** + - **Issue**: Line 28 had incorrect title and calculation logic + - **Requirements**: Title should be 'TVA nette dûe (ligne 16 - ligne 23)', calculation should be line 16 - line 23, only display if > 0 + - **Root Cause**: Line 28 was using incorrect calculation logic based on net VAT due instead of line 16 - line 23 + - **Solution**: + - Modified `calculateDSectionLines()` to calculate line 28 as line 16 - line 23 + - Updated title to 'TVA nette dûe (ligne 16 - ligne 23)' + - Only display value if > 0 + - **Files Modified**: `core/class/declarationtva.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 30 minutes + +### Bug Fixes - CA-3 Line 28 Calculation (Updated) +- [x] **Task Bug-5**: Update line 28 to use same value as line TD ✅ **COMPLETED** + - **Issue**: Line 28 needed to be updated to use same value as line TD + - **Requirements**: Title should be 'TVA nette due (ligne TD – ligne X5)', value should be same as line TD, only display if > 0 + - **Root Cause**: Line 28 was calculating independently instead of using line TD value + - **Solution**: + - Modified `calculateDSectionLines()` to use `$line_td_amount` for line 28 + - Updated title to 'TVA nette due (ligne TD – ligne X5)' + - Line 28 now shows same value as line TD + - **Files Modified**: `core/class/declarationtva.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 15 minutes + +### Bug Fixes - CA-3 Line TD Calculation +- [x] **Task Bug-4**: Fix line TD calculation and title ✅ **COMPLETED** + - **Issue**: Line TD had incorrect title and calculation logic + - **Requirements**: Title should be 'TVA due (ligne 16 – ligne 23)', calculation should be line 16 - line 23, only display if > 0 + - **Root Cause**: Line TD was using incorrect calculation logic based on net VAT due instead of line 16 - line 23 + - **Solution**: + - Modified `calculateDSectionLines()` to calculate line TD as line 16 - line 23 + - Updated title to 'TVA due (ligne 16 – ligne 23)' + - Only display value if > 0 + - Removed special handling in view to use database line_label + - **Files Modified**: `core/class/declarationtva.class.php`, `declarationtva_view.php` + - **Date**: 2025-01-27 + - **Estimated**: 30 minutes + +### Bug Fixes - Remove Lines 26 and 29 +- [x] **Task Bug-6**: Remove lines 26 and 29 from CA-3 calculation ✅ **COMPLETED** + - **Issue**: Lines 26 and 29 are no longer needed in the CA-3 form + - **Requirements**: Remove lines 26 and 29 from both calculation and display + - **Root Cause**: Lines 26 and 29 were part of the original CA-3 structure but are no longer required + - **Solution**: + - Removed line 26 calculation logic from `calculateDSectionLines()` + - Removed line 29 calculation logic from `calculateDSectionLines()` + - Updated view to remove lines 26 and 29 from `$section_d_lines` array + - Section D now only shows lines 25, TD, and 28 + - **Files Modified**: `core/class/declarationtva.class.php`, `declarationtva_view.php` + - **Date**: 2025-01-27 + - **Estimated**: 15 minutes + +### Bug Fixes - Add Line 32 +- [x] **Task Bug-7**: Add line 32 to CA-3 calculation ✅ **COMPLETED** + - **Issue**: Need to add line 32 for total amount to pay + - **Requirements**: Title should be 'Total à payer (lignes 28 + 29 + Z5 – AB) (N'oubliez pas d'effectuer le règlement correspondant)', value should be same as line 28, only show if > 0, use bold formatting + - **Root Cause**: Line 32 was missing from the CA-3 form structure + - **Solution**: + - Added line 32 calculation logic to `calculateDSectionLines()` + - Line 32 uses same value as line 28 + - Added line 32 to `$section_d_lines` array in view + - Added special bold formatting for line 32 in view display + - Section D now shows lines 25, TD, 28, and 32 + - **Files Modified**: `core/class/declarationtva.class.php`, `declarationtva_view.php` + - **Date**: 2025-01-27 + - **Estimated**: 20 minutes + +### Bug Fixes - Add Line 27 +- [x] **Task Bug-8**: Add line 27 to CA-3 calculation ✅ **COMPLETED** + - **Issue**: Need to add line 27 for VAT credit to carry forward + - **Requirements**: Title should be 'Crédit de TVA à reporter (ligne 25 – ligne 26) (Cette somme est à reporter ligne 22 de la prochaine déclaration)', value should be same as line 25, only show if > 0 + - **Root Cause**: Line 27 was missing from the CA-3 form structure + - **Solution**: + - Added line 27 calculation logic to `calculateDSectionLines()` + - Line 27 uses same value as line 25 + - Added line 27 to `$section_d_lines` array in view + - Section D now shows lines 25, 27, TD, 28, and 32 + - **Files Modified**: `core/class/declarationtva.class.php`, `declarationtva_view.php` + - **Date**: 2025-01-27 + - **Estimated**: 15 minutes + +### Bug Fixes - PDF Field Mapping +- [x] **Task Bug-9**: Add missing lines to PDF field mapping ✅ **COMPLETED** + - **Issue**: New CA-3 lines (27, 32) were not included in PDF generation + - **Requirements**: All new lines should be available in generated PDF + - **Root Cause**: PDF field mapping was missing lines 27 and 32 + - **Solution**: + - Added `D27_amount` field mapping for line 27 + - Added `D32_amount` field mapping for line 32 + - Updated debug logging to include new lines + - Removed obsolete lines 26 and 29 from PDF mapping + - **Files Modified**: `core/class/declarationtva_pdf.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 10 minutes + +### Bug Fixes - Add Line 26 and Conditional Visibility +- [x] **Task Bug-10**: Add line 26 and conditional visibility logic ✅ **COMPLETED** + - **Issue**: Need line 26 for VAT credit refund and conditional visibility for line 27 + - **Requirements**: + - Line 27: visible if > 0 and < 760 + - Line 26: visible if >= 760 + - Add line 26 to PDF fields + - **Root Cause**: Missing conditional logic for VAT credit handling + - **Solution**: + - Added line 26 calculation with visibility >= 760 + - Modified line 27 visibility to only show if > 0 and < 760 + - Added `D26_amount` to PDF field mapping + - Updated view to include line 26 in display + - Both lines use same value as line 25 but with different visibility rules + - **Files Modified**: `core/class/declarationtva.class.php`, `declarationtva_view.php`, `core/class/declarationtva_pdf.class.php` + - **Date**: 2025-01-27 + - **Estimated**: 20 minutes --- diff --git a/admin/setup_mvp.php b/admin/setup_mvp.php index 0f161bf..741e6ea 100644 --- a/admin/setup_mvp.php +++ b/admin/setup_mvp.php @@ -47,30 +47,25 @@ if ($action == 'update_mappings') { $base_account_codes = GETPOST('base_account_codes_' . $line, 'array'); $vat_account_codes = GETPOST('vat_account_codes_' . $line, 'array'); - // Process base accounts - if (isset($_POST['base_account_codes_' . $line])) { - $result = $config->updateAccountMapping($line . '_BASE', $base_account_codes); - if ($result) { - $updated_count++; - } + // Always process base accounts (even if empty) + $result = $config->updateAccountMapping($line . '_BASE', $base_account_codes); + if ($result) { + $updated_count++; } - // Process VAT accounts - if (isset($_POST['vat_account_codes_' . $line])) { - $result = $config->updateAccountMapping($line . '_VAT', $vat_account_codes); - if ($result) { - $updated_count++; - } + // Always process VAT accounts (even if empty) + $result = $config->updateAccountMapping($line . '_VAT', $vat_account_codes); + if ($result) { + $updated_count++; } } else { // Normal processing for other lines $account_codes = GETPOST('account_codes_' . $line, 'array'); - if (isset($_POST['account_codes_' . $line])) { - $result = $config->updateAccountMapping($line, $account_codes); - if ($result) { - $updated_count++; - } + // Always process account mappings (even if empty) + $result = $config->updateAccountMapping($line, $account_codes); + if ($result) { + $updated_count++; } } } diff --git a/core/class/declarationtva.class.php b/core/class/declarationtva.class.php index 99e918b..1e847cf 100644 --- a/core/class/declarationtva.class.php +++ b/core/class/declarationtva.class.php @@ -395,6 +395,28 @@ class DeclarationTVA return true; } + /** + * Get amount for a specific CA-3 line + * + * @param int $declaration_id Declaration ID + * @param string $ca3_line CA-3 line code + * @return float Line amount + */ + private function getLineAmount($declaration_id, $ca3_line) + { + $sql = "SELECT vat_amount FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines + WHERE declaration_id = " . (int)$declaration_id . " + AND ca3_line = '" . $this->db->escape($ca3_line) . "'"; + + $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.0; + } + /** * Calculate D-section result lines (25, 26, 28, 29) * @@ -405,74 +427,103 @@ class DeclarationTVA */ private function calculateDSectionLines($declaration_id, $total_vat_collected, $total_vat_deductible) { + // Get line 16 and 23 amounts for line 25 calculation + $line_16_amount = $this->getLineAmount($declaration_id, '16'); + $line_23_amount = $this->getLineAmount($declaration_id, '23'); + // Calculate net VAT due $net_vat_due = $total_vat_collected - $total_vat_deductible; - // Line 25: TVA brute due (Total VAT due) - only if negative (VAT credit - we receive money) - if ($net_vat_due < 0) { - $line_25_amount = $total_vat_collected; - $this->createCA3Line($declaration_id, '25', 'Calculated from sections A and B', array( + // Line 25: Crédit de TVA (ligne 23 - ligne16) - only if > 0 + $line_25_amount = $line_23_amount - $line_16_amount; + if ($line_25_amount > 0) { + $this->createCA3Line($declaration_id, '25', 'Crédit de TVA (ligne 23 - ligne16)', array( 'base_amount' => 0, 'vat_amount' => $line_25_amount, 'total_amount' => $line_25_amount )); } else { - $this->createCA3Line($declaration_id, '25', 'Calculated from sections A and B', array( + $this->createCA3Line($declaration_id, '25', 'Crédit de TVA (ligne 23 - ligne16)', array( 'base_amount' => 0, 'vat_amount' => 0, 'total_amount' => 0 )); } - // Line 26: TVA déductible totale (Total deductible VAT) - $line_26_amount = $total_vat_deductible; - $this->createCA3Line($declaration_id, '26', 'Calculated from section C', array( - 'base_amount' => 0, - 'vat_amount' => $line_26_amount, - 'total_amount' => $line_26_amount - )); + // Line 27: Crédit de TVA à reporter (ligne 25 – ligne 26) - same value as line 25, only if > 0 and < 760 + $line_27_amount = $line_25_amount; // Same value as line 25 + if ($line_27_amount > 0 && $line_27_amount < 760) { + $this->createCA3Line($declaration_id, '27', 'Crédit de TVA à reporter (ligne 25 – ligne 26) (Cette somme est à reporter ligne 22 de la prochaine déclaration)', array( + 'base_amount' => 0, + 'vat_amount' => $line_27_amount, + 'total_amount' => $line_27_amount + )); + } else { + $this->createCA3Line($declaration_id, '27', 'Crédit de TVA à reporter (ligne 25 – ligne 26) (Cette somme est à reporter ligne 22 de la prochaine déclaration)', array( + 'base_amount' => 0, + 'vat_amount' => 0, + 'total_amount' => 0 + )); + } - // Line TD: TVA due (VAT due) - only if positive (we need to pay) - if ($net_vat_due > 0) { - $line_td_amount = $net_vat_due; - $this->createCA3Line($declaration_id, 'TD', 'TVA due (amount to pay)', array( + // Line 26: Remboursement de crédit de TVA demandé sur formulaire n°3519 - same value as line 25, only if >= 760 + $line_26_amount = $line_25_amount; // Same value as line 25 + if ($line_26_amount >= 760) { + $this->createCA3Line($declaration_id, '26', 'Remboursement de crédit de TVA demandé sur formulaire n°3519', array( + 'base_amount' => 0, + 'vat_amount' => $line_26_amount, + 'total_amount' => $line_26_amount + )); + } else { + $this->createCA3Line($declaration_id, '26', 'Remboursement de crédit de TVA demandé sur formulaire n°3519', array( + 'base_amount' => 0, + 'vat_amount' => 0, + 'total_amount' => 0 + )); + } + + // Line TD: TVA due (ligne 16 – ligne 23) - only if > 0 + $line_td_amount = $line_16_amount - $line_23_amount; + if ($line_td_amount > 0) { + $this->createCA3Line($declaration_id, 'TD', 'TVA due (ligne 16 – ligne 23)', array( 'base_amount' => 0, 'vat_amount' => $line_td_amount, 'total_amount' => $line_td_amount )); } else { - $this->createCA3Line($declaration_id, 'TD', 'TVA due (amount to pay)', array( + $this->createCA3Line($declaration_id, 'TD', 'TVA due (ligne 16 – ligne 23)', array( 'base_amount' => 0, 'vat_amount' => 0, 'total_amount' => 0 )); } - // Line 28: TVA nette due (Net VAT due) - if positive - if ($net_vat_due > 0) { - $this->createCA3Line($declaration_id, '28', 'Calculated: 25 - 26', array( + // Line 28: TVA nette due (ligne TD – ligne X5) - same value as line TD, only if > 0 + $line_28_amount = $line_td_amount; // Same value as line TD + if ($line_28_amount > 0) { + $this->createCA3Line($declaration_id, '28', 'TVA nette due (ligne TD – ligne X5)', array( 'base_amount' => 0, - 'vat_amount' => $net_vat_due, - 'total_amount' => $net_vat_due + 'vat_amount' => $line_28_amount, + 'total_amount' => $line_28_amount )); } else { - $this->createCA3Line($declaration_id, '28', 'Calculated: 25 - 26', array( + $this->createCA3Line($declaration_id, '28', 'TVA nette due (ligne TD – ligne X5)', array( 'base_amount' => 0, 'vat_amount' => 0, 'total_amount' => 0 )); } - // Line 29: Crédit de TVA (VAT Credit) - if negative - if ($net_vat_due < 0) { - $vat_credit = abs($net_vat_due); - $this->createCA3Line($declaration_id, '29', 'Calculated: 26 - 25', array( + // Line 32: Total à payer (lignes 28 + 29 + Z5 – AB) - same value as line 28, only if > 0 + $line_32_amount = $line_28_amount; // Same value as line 28 + if ($line_32_amount > 0) { + $this->createCA3Line($declaration_id, '32', 'Total à payer (lignes 28 + 29 + Z5 – AB) (N\'oubliez pas d\'effectuer le règlement correspondant)', array( 'base_amount' => 0, - 'vat_amount' => $vat_credit, - 'total_amount' => $vat_credit + 'vat_amount' => $line_32_amount, + 'total_amount' => $line_32_amount )); } else { - $this->createCA3Line($declaration_id, '29', 'Calculated: 26 - 25', array( + $this->createCA3Line($declaration_id, '32', 'Total à payer (lignes 28 + 29 + Z5 – AB) (N\'oubliez pas d\'effectuer le règlement correspondant)', array( 'base_amount' => 0, 'vat_amount' => 0, 'total_amount' => 0 diff --git a/core/class/declarationtva_config.class.php b/core/class/declarationtva_config.class.php index d81e6d3..f3a5ab6 100644 --- a/core/class/declarationtva_config.class.php +++ b/core/class/declarationtva_config.class.php @@ -108,8 +108,8 @@ class DeclarationTVA_Config WHERE entity = " . $this->entity . " AND ca3_line = '" . $this->db->escape($ca3_line) . "'"; $this->db->query($sql); - // Then insert/activate new mappings - if (!empty($account_codes)) { + // Then insert/activate new mappings (only if account_codes is not empty) + if (!empty($account_codes) && is_array($account_codes)) { foreach ($account_codes as $account_code) { if (!empty($account_code)) { // Check if mapping already exists (active or inactive) @@ -138,6 +138,8 @@ class DeclarationTVA_Config } } } + // If account_codes is empty, all mappings for this line remain deactivated + // This ensures that clearing all selections properly saves the empty state return true; } diff --git a/core/class/declarationtva_pdf.class.php b/core/class/declarationtva_pdf.class.php index 6ebd070..a83cea8 100644 --- a/core/class/declarationtva_pdf.class.php +++ b/core/class/declarationtva_pdf.class.php @@ -149,6 +149,55 @@ class DeclarationTVA_PDF return false; } } + + /** + * 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 = '') + { + 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_Detailed_' . $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/'); + } + + // Generate detailed PDF with breakdown pages + $result = $this->generateDetailedPDF($filepath, $declaration, $ca3_data, $company); + + if ($result) { + return $filepath; + } else { + return false; + } + } /** * Get the template file path (custom or default) @@ -247,6 +296,8 @@ class DeclarationTVA_PDF // 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, $company); return true; } else { $this->error = 'pdftk failed to generate output: ' . $result; @@ -373,9 +424,10 @@ class DeclarationTVA_PDF // 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['D29_amount'] = $this->getCA3LineAmount($ca3_data, '29'); + $field_data['D32_amount'] = $this->getCA3LineAmount($ca3_data, '32'); // Subtotals $field_data['subtotal_B16_amount'] = $this->getCA3LineAmount($ca3_data, '16'); @@ -388,7 +440,7 @@ class DeclarationTVA_PDF $field_data['vat_credit'] = $this->formatAmount($declaration->vat_credit); // Debug: Log specific D-section fields - error_log("DeclarationTVA: D-section fields - D25: " . $field_data['D25_amount'] . ", D26: " . $field_data['D26_amount'] . ", DTD: " . $field_data['DTD_amount'] . ", D28: " . $field_data['D28_amount'] . ", D29: " . $field_data['D29_amount']); + error_log("DeclarationTVA: D-section fields - D25: " . $field_data['D25_amount'] . ", D26: " . $field_data['D26_amount'] . ", D27: " . $field_data['D27_amount'] . ", DTD: " . $field_data['DTD_amount'] . ", D28: " . $field_data['D28_amount'] . ", D32: " . $field_data['D32_amount']); return $field_data; } @@ -705,6 +757,9 @@ class DeclarationTVA_PDF file_put_contents($data_file, $data_content); + // Add detailed pages to the filled PDF + $this->addDetailedPagesToPDF($output_path, $declaration, $ca3_data, $company); + return true; } else { $this->error = 'Failed to copy template file'; @@ -756,14 +811,14 @@ class DeclarationTVA_PDF $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->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', 'TD', '28', '29')); + $this->addCA3Section($pdf, 'D. Résultat', $ca3_data, array('25', '26', '27', 'TD', '28', '32')); // Add totals $pdf->Ln(10); @@ -776,6 +831,9 @@ class DeclarationTVA_PDF $pdf->Cell(0, 8, 'CRÉDIT DE TVA: ' . price($declaration->vat_credit, 0, '', 1, 0), 0, 1); } + // Add detailed breakdown pages + $this->addDetailPages($pdf, $declaration, $ca3_data); + // Output PDF $pdf->Output($output_path, 'F'); @@ -805,7 +863,12 @@ class DeclarationTVA_PDF 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'); + + // 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'); } @@ -813,6 +876,554 @@ class DeclarationTVA_PDF $pdf->Ln(5); } + /** + * Add detailed breakdown pages to PDF + * + * @param TCPDF $pdf PDF object + * @param DeclarationTVA $declaration Declaration object + * @param array $ca3_data CA-3 line data + */ + private function addDetailPages($pdf, $declaration, $ca3_data) + { + // Define calculated lines that should be excluded from detailed breakdown + $calculated_lines = array('25', '26', '27', 'TD', '28', '32', '16', '23'); + + // Define the order of lines as they appear in the view page + $ordered_lines = array( + // Section A: Opérations imposables + 'A1', 'A2', 'A3', 'A4', 'A5', 'E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'F1', 'F2', 'F6', 'F7', 'F8', + // Section B: TVA due + '08', '09', '9B', '17', + // Section C: TVA déductible + '19', '20', '21', '22' + ); + + // Create a lookup array for quick access to line data + $ca3_lookup = array(); + foreach ($ca3_data as $line_data) { + $ca3_lookup[$line_data['ca3_line']] = $line_data; + } + + // Get lines that have actual values and are not calculated lines, in the correct order + $lines_with_data = array(); + foreach ($ordered_lines as $line_code) { + // Skip if line doesn't exist in data + if (!isset($ca3_lookup[$line_code])) { + continue; + } + + // Skip calculated lines - they don't have account mappings + if (in_array($line_code, $calculated_lines)) { + continue; + } + + $line_data = $ca3_lookup[$line_code]; + $has_vat_amount = isset($line_data['vat_amount']) && $line_data['vat_amount'] > 0; + $has_base_amount = isset($line_data['base_amount']) && $line_data['base_amount'] > 0; + $has_total_amount = isset($line_data['total_amount']) && $line_data['total_amount'] > 0; + + // Only include lines that have meaningful values and are not calculated + if ($has_vat_amount || $has_base_amount || $has_total_amount) { + $lines_with_data[] = $line_code; + } + } + + // If no lines have data, add a message page + if (empty($lines_with_data)) { + $pdf->AddPage(); + $pdf->SetFont('helvetica', 'B', 16); + $pdf->Cell(0, 10, 'Aucune donnée détaillée disponible', 0, 1, 'C'); + $pdf->Ln(10); + $pdf->SetFont('helvetica', '', 12); + $pdf->Cell(0, 8, 'Aucune ligne de la déclaration ne contient de données comptables.', 0, 1); + $pdf->Cell(0, 8, 'Veuillez configurer les mappings de comptes dans la section Administration.', 0, 1); + return; + } + + // Add a detail section for each line with data in the correct order + foreach ($lines_with_data as $line_code) { + $this->addLineDetailPage($pdf, $declaration, $line_code); + } + } + + /** + * Add a detail section for a specific CA-3 line + * + * @param TCPDF $pdf PDF object + * @param DeclarationTVA $declaration Declaration object + * @param string $line_code CA-3 line code + */ + private function addLineDetailPage($pdf, $declaration, $line_code) + { + // Check if we need a new page (if current position is too low) + $current_y = $pdf->GetY(); + $page_height = $pdf->getPageHeight() - $pdf->getMargins()['bottom']; + + // If we're too close to the bottom, start a new page + if ($current_y > $page_height - 100) { + $pdf->AddPage(); + } else { + // Add some space between sections + $pdf->Ln(10); + } + + // Set section title with separator + $pdf->SetFont('helvetica', 'B', 16); + $pdf->SetTextColor(0, 0, 0); + $pdf->Cell(0, 8, 'Détail de la ligne ' . $line_code, 0, 1, 'C'); + + // Add separator line + $pdf->SetDrawColor(0, 0, 0); + $pdf->SetLineWidth(0.5); + $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 + $pdf->SetFont('helvetica', 'B', 12); + $pdf->Cell(0, 8, 'Résumé de la ligne ' . $line_code, 0, 1); + + $pdf->SetFont('helvetica', '', 10); + $pdf->Cell(0, 6, 'Période: ' . dol_print_date($line_details['start_date'], 'day') . ' - ' . dol_print_date($line_details['end_date'], 'day'), 0, 1); + $pdf->Cell(0, 6, 'Nombre de comptes: ' . $line_details['account_count'], 0, 1); + + if (!empty($line_details['calculated_line'])) { + $calc = $line_details['calculated_line']; + $pdf->Cell(0, 6, 'Montant calculé: ' . price($calc->vat_amount, 0, '', 1, 0), 0, 1); + } + + $pdf->Ln(10); + + // Add account details table + $this->addAccountDetailsTable($pdf, $line_details['account_details'], $line_code); + + } + + /** + * Add account details table to PDF + * + * @param TCPDF $pdf PDF object + * @param array $account_details Account details array + * @param string $line_code CA-3 line code + */ + private function addAccountDetailsTable($pdf, $account_details, $line_code) + { + // Group accounts by type for lines 08, 09, 9B (same as view page) + $base_accounts = array(); + $vat_accounts = array(); + $other_accounts = array(); + + foreach ($account_details as $account) { + 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 + $table_width = 150; // 35 + 80 + 35 + $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 - same as view page: Code compte, Libellé compte, Montant + $pdf->SetFont('helvetica', 'B', 10); + $pdf->Cell(35, 8, 'Code compte', 1, 0, 'C'); + $pdf->Cell(80, 8, 'Libellé compte', 1, 0, 'C'); + $pdf->Cell(35, 8, 'Montant', 1, 1, 'C'); + + $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', 9); + $pdf->Cell(0, 6, 'Comptes de base (ventes)', 0, 1, 'L'); + + $pdf->SetFont('helvetica', '', 9); + foreach ($base_accounts as $account) { + $total_base_base += $account['base_amount']; + $total_vat_base += $account['vat_amount']; + + $pdf->SetX($start_x); + $pdf->Cell(35, 6, $account['account_code'], 1, 0, 'C'); + $pdf->Cell(80, 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L'); + $pdf->Cell(35, 6, price($account['base_amount'], 0, '', 1, 0), 1, 1, 'R'); + } + + // Subtotal for BASE accounts + $pdf->SetFont('helvetica', 'B', 9); + $pdf->SetX($start_x); + $pdf->Cell(115, 6, 'Sous-total comptes de base', 1, 0, 'L'); + $pdf->Cell(35, 6, price($total_base_base, 0, '', 1, 0), 1, 1, 'R'); + $pdf->Ln(2); + } + + // Display VAT accounts second (if any) + if (!empty($vat_accounts)) { + $pdf->SetFont('helvetica', 'B', 9); + $pdf->Cell(0, 6, 'Comptes de TVA', 0, 1, 'L'); + + $pdf->SetFont('helvetica', '', 9); + foreach ($vat_accounts as $account) { + $total_base_vat += $account['base_amount']; + $total_vat_vat += $account['vat_amount']; + + $pdf->SetX($start_x); + $pdf->Cell(35, 6, $account['account_code'], 1, 0, 'C'); + $pdf->Cell(80, 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L'); + $pdf->Cell(35, 6, price($account['vat_amount'], 0, '', 1, 0), 1, 1, 'R'); + } + + // Subtotal for VAT accounts + $pdf->SetFont('helvetica', 'B', 9); + $pdf->SetX($start_x); + $pdf->Cell(115, 6, 'Sous-total comptes de TVA', 1, 0, 'L'); + $pdf->Cell(35, 6, price($total_vat_vat, 0, '', 1, 0), 1, 1, 'R'); + $pdf->Ln(2); + } + + // Display other accounts (normal lines) + if (!empty($other_accounts)) { + $pdf->SetFont('helvetica', '', 9); + foreach ($other_accounts as $account) { + $total_base_other += $account['base_amount']; + $total_vat_other += $account['vat_amount']; + + $pdf->SetX($start_x); + $pdf->Cell(35, 6, $account['account_code'], 1, 0, 'C'); + $pdf->Cell(80, 6, $this->truncateText($account['account_label'], 35), 1, 0, 'L'); + $pdf->Cell(35, 6, price($account['vat_amount'], 0, '', 1, 0), 1, 1, 'R'); + } + + // Total for other accounts + if (!empty($other_accounts)) { + $pdf->SetFont('helvetica', 'B', 9); + $pdf->SetX($start_x); + $pdf->Cell(115, 6, 'Total', 1, 0, 'L'); + $pdf->Cell(35, 6, price($total_vat_other, 0, '', 1, 0), 1, 1, 'R'); + } + } + } + + /** + * 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, $company) + { + try { + error_log("DeclarationTVA: Starting addDetailedPagesToPDF process"); + + // Create a temporary detailed PDF + $temp_detailed_path = tempnam(sys_get_temp_dir(), 'ca3_detailed_') . '.pdf'; + error_log("DeclarationTVA: Temporary detailed PDF path: " . $temp_detailed_path); + + $result = $this->generateDetailedPDF($temp_detailed_path, $declaration, $ca3_data, $company); + + if (!$result) { + error_log("DeclarationTVA: Failed to generate detailed PDF: " . $this->error); + return false; + } + + // Check if detailed PDF was created and has content + if (!file_exists($temp_detailed_path)) { + error_log("DeclarationTVA: Detailed PDF file does not exist at: " . $temp_detailed_path); + return false; + } + + if (filesize($temp_detailed_path) == 0) { + error_log("DeclarationTVA: Detailed PDF file is empty"); + return false; + } + + error_log("DeclarationTVA: Detailed PDF created successfully, size: " . filesize($temp_detailed_path) . " bytes"); + + // 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) { + error_log("DeclarationTVA: Failed to merge PDFs with pdftk: " . $this->error); + // Fallback: try FPDI merge + $merge_result = $this->mergePDFs($pdf_path, $temp_detailed_path, $pdf_path); + if (!$merge_result) { + error_log("DeclarationTVA: Failed to merge PDFs with FPDI: " . $this->error); + return false; + } + } + + error_log("DeclarationTVA: PDFs merged successfully"); + + // 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(); + error_log("DeclarationTVA: Exception in addDetailedPagesToPDF: " . $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; + } + + error_log("DeclarationTVA: Starting PDF merge with pdftk - PDF1: " . $pdf1_path . " (size: " . filesize($pdf1_path) . "), PDF2: " . $pdf2_path . " (size: " . filesize($pdf2_path) . ")"); + + // 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\""; + error_log("DeclarationTVA: pdftk command: " . $command); + $result = shell_exec($command . ' 2>&1'); + error_log("DeclarationTVA: pdftk result: " . $result); + + // 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) { + error_log("DeclarationTVA: PDFs merged successfully with pdftk, size: " . filesize($output_path) . " bytes"); + return true; + } else { + $this->error = 'pdftk failed to merge PDFs: ' . $result; + error_log("DeclarationTVA: pdftk merge failed: " . $result); + return false; + } + + } catch (Exception $e) { + $this->error = 'Failed to merge PDFs with pdftk: ' . $e->getMessage(); + error_log("DeclarationTVA: Exception in mergePDFsWithPdftk: " . $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 { + error_log("DeclarationTVA: Starting PDF merge - PDF1: " . $pdf1_path . " (size: " . filesize($pdf1_path) . "), PDF2: " . $pdf2_path . " (size: " . filesize($pdf2_path) . ")"); + + // Use TCPDF-based FPDI + $pdf = new \setasign\Fpdi\Tcpdf\Fpdi(); + + // Import pages from first PDF + $page_count1 = $pdf->setSourceFile($pdf1_path); + error_log("DeclarationTVA: PDF1 has " . $page_count1 . " pages"); + 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); + error_log("DeclarationTVA: PDF2 has " . $page_count2 . " pages"); + for ($i = 1; $i <= $page_count2; $i++) { + $pdf->AddPage(); + $pdf->useTemplate($pdf->importPage($i)); + } + + // Output merged PDF + $pdf->Output($output_path, 'F'); + + error_log("DeclarationTVA: Merged PDF created successfully, size: " . filesize($output_path) . " bytes"); + + return true; + + } catch (Exception $e) { + $this->error = 'Failed to merge PDFs: ' . $e->getMessage(); + error_log("DeclarationTVA: Exception in mergePDFs: " . $e->getMessage()); + return false; + } + } + + /** + * Translate status to French + * + * @param string $status Status in English + * @return string Status in French + */ + private function translateStatus($status) + { + $translations = array( + 'draft' => 'Brouillon', + 'validated' => 'Validé', + 'submitted' => 'Soumis', + 'approved' => 'Approuvé', + 'rejected' => 'Rejeté' + ); + + return isset($translations[$status]) ? $translations[$status] : ucfirst($status); + } + + /** + * Get mapping type label for display + * + * @param string $mapping_type Mapping type (e.g., '08_BASE', '08_VAT') + * @return string Display label + */ + private function getMappingTypeLabel($mapping_type) + { + if (strpos($mapping_type, '_BASE') !== false) { + return 'Base'; + } elseif (strpos($mapping_type, '_VAT') !== false) { + return 'TVA'; + } else { + return 'Standard'; + } + } + + /** + * Truncate text to fit in table cell + * + * @param string $text Text to truncate + * @param int $max_length Maximum length + * @return string Truncated text + */ + private function truncateText($text, $max_length) + { + if (strlen($text) <= $max_length) { + return $text; + } + return substr($text, 0, $max_length - 3) . '...'; + } + + /** + * Get detail page number for a line (for linking) + * + * @param string $line_code CA-3 line code + * @return int Page number + */ + private function getDetailPageNumber($line_code) + { + // This is a simplified approach - in a real implementation, + // you'd track page numbers as you create them + return 2; // Start from page 2 (after main form) + } + + /** + * Generate detailed PDF with breakdown pages + * + * @param string $output_path Output file path + * @param DeclarationTVA $declaration Declaration object + * @param array $ca3_data CA-3 line data + * @param Societe $company Company object + * @return bool Success + */ + private function generateDetailedPDF($output_path, $declaration, $ca3_data, $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 Details ' . $declaration->declaration_number); + $pdf->SetSubject('French VAT Declaration - Detailed Breakdown'); + + // Set margins + $pdf->SetMargins(15, 15, 15); + $pdf->SetHeaderMargin(5); + $pdf->SetFooterMargin(10); + + // Add title page + $pdf->AddPage(); + $pdf->SetFont('helvetica', 'B', 16); + $pdf->Cell(0, 10, 'Détails de la Déclaration TVA CA-3', 0, 1, 'C'); + $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 detailed breakdown pages + $this->addDetailPages($pdf, $declaration, $ca3_data); + + // Output PDF + $pdf->Output($output_path, 'F'); + + return true; + + } catch (Exception $e) { + $this->error = 'Detailed PDF generation failed: ' . $e->getMessage(); + return false; + } + } + /** * Upload custom template * diff --git a/core/modules/modDeclarationTVA.class.php b/core/modules/modDeclarationTVA.class.php index 2aff0fb..5e1d83c 100644 --- a/core/modules/modDeclarationTVA.class.php +++ b/core/modules/modDeclarationTVA.class.php @@ -76,7 +76,7 @@ class modDeclarationTVA extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@declarationtva' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '2.0.0'; + $this->version = '2.1.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/declarationtva_view.php b/declarationtva_view.php index 7c66d0a..f1b35d0 100644 --- a/declarationtva_view.php +++ b/declarationtva_view.php @@ -76,6 +76,28 @@ if ($action == 'export_pdf') { } } +if ($action == 'export_pdf_detailed') { + // Load PDF generator + require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_pdf.class.php'; + $pdf_generator = new DeclarationTVA_PDF($db); + + // Generate detailed PDF with breakdown pages + $pdf_path = $pdf_generator->generateDetailedCA3PDF($id); + + if ($pdf_path && file_exists($pdf_path)) { + // Set headers for PDF download + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="CA3_Detailed_' . $declarationtva->declaration_number . '.pdf"'); + header('Content-Length: ' . filesize($pdf_path)); + + // Output PDF file + readfile($pdf_path); + exit; + } else { + setEventMessages($pdf_generator->error ?: $langs->trans("ErrorGeneratingDetailedPDF"), null, 'errors'); + } +} + // Fetch declaration if ($declarationtva->fetch($id) < 0) { setEventMessages($langs->trans("DeclarationNotFound"), null, 'errors'); @@ -353,21 +375,21 @@ print '