- Removed duplicate F1/F2 field mappings that were overwriting correct values - Updated all remaining fields to use getCA3LineAmount() helper method - Fixed D25, D26, DTD, D28, D29, subtotal_B16, subtotal_C23 fields - All fields now consistently use the correct array structure lookup - F2 should now display correct amount instead of 0,00
1081 lines
42 KiB
PHP
1081 lines
42 KiB
PHP
<?php
|
|
/* Copyright (C) 2025 Frank Cools
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file core/class/declarationtva_pdf.class.php
|
|
* \ingroup declarationtva
|
|
* \brief PDF generation for CA-3 declarations
|
|
*/
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/pdf.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
|
|
|
|
// Load TCPDF for PDF support (Dolibarr's standard)
|
|
require_once DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php';
|
|
|
|
// Load FPDI-TCPDF for fillable PDF support
|
|
require_once DOL_DOCUMENT_ROOT.'/custom/declarationtva/vendor/autoload.php';
|
|
|
|
/**
|
|
* Class to generate CA-3 declaration PDF
|
|
*/
|
|
class DeclarationTVA_PDF
|
|
{
|
|
/**
|
|
* @var DoliDB Database handler
|
|
*/
|
|
public $db;
|
|
|
|
/**
|
|
* @var string Error code (or message)
|
|
*/
|
|
public $error = '';
|
|
|
|
/**
|
|
* @var string[] Several error codes (or messages)
|
|
*/
|
|
public $errors = array();
|
|
|
|
/**
|
|
* @var int Entity
|
|
*/
|
|
public $entity;
|
|
|
|
/**
|
|
* @var string Template path
|
|
*/
|
|
public $template_path;
|
|
|
|
/**
|
|
* @var string Template version
|
|
*/
|
|
public $template_version = '30';
|
|
|
|
/**
|
|
* @var string Template document number
|
|
*/
|
|
public $template_document = '10963';
|
|
|
|
/**
|
|
* @var string Gitea repository URL for templates
|
|
*/
|
|
public $gitea_repo_url = 'https://git.covago.com/frank/DeclarationTVA';
|
|
|
|
/**
|
|
* @var string Manifest file URL
|
|
*/
|
|
public $manifest_url = 'https://git.covago.com/frank/DeclarationTVA/raw/branch/main/templates/manifest.json';
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
$this->entity = (int) $conf->entity;
|
|
$this->template_path = DOL_DOCUMENT_ROOT.'/custom/declarationtva/templates/declarationtva/';
|
|
}
|
|
|
|
/**
|
|
* Generate CA-3 declaration PDF
|
|
*
|
|
* @param int $declaration_id Declaration ID
|
|
* @param string $outputlangs Output language
|
|
* @return string|false PDF file path or false on error
|
|
*/
|
|
public function generateCA3PDF($declaration_id, $outputlangs = '')
|
|
{
|
|
global $conf, $langs, $user;
|
|
|
|
// Load declaration data
|
|
$declaration = new DeclarationTVA($this->db);
|
|
$result = $declaration->fetch($declaration_id);
|
|
if ($result <= 0) {
|
|
$this->error = 'Declaration not found';
|
|
return false;
|
|
}
|
|
|
|
// Get CA-3 line data
|
|
$ca3_data = $declaration->getCA3Lines($declaration_id);
|
|
if (empty($ca3_data)) {
|
|
$this->error = 'No CA-3 data found';
|
|
return false;
|
|
}
|
|
|
|
// Get company information
|
|
$company = new Societe($this->db);
|
|
$company->fetch($conf->entity);
|
|
|
|
// Generate PDF filename
|
|
$filename = 'CA3_' . $declaration->declaration_number . '_' . date('Y-m-d') . '.pdf';
|
|
$filepath = DOL_DATA_ROOT . '/declarationtva/' . $filename;
|
|
|
|
// Ensure directory exists
|
|
if (!is_dir(DOL_DATA_ROOT . '/declarationtva/')) {
|
|
dol_mkdir(DOL_DATA_ROOT . '/declarationtva/');
|
|
}
|
|
|
|
// Check if we have a custom template
|
|
$template_file = $this->getTemplatePath();
|
|
if (!$template_file) {
|
|
$this->error = 'CA-3 template not found';
|
|
return false;
|
|
}
|
|
|
|
// Generate PDF using FPDI or similar library
|
|
$result = $this->fillPDFTemplate($template_file, $filepath, $declaration, $ca3_data, $company);
|
|
|
|
if ($result) {
|
|
return $filepath;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the template file path (custom or default)
|
|
*
|
|
* @return string|false Template file path or false if not found
|
|
*/
|
|
private function getTemplatePath()
|
|
{
|
|
// Check for custom template first
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
|
|
// Fall back to default template
|
|
$default_template = $this->template_path . 'ca3_official_template.pdf';
|
|
if (file_exists($default_template)) {
|
|
return $default_template;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Fill PDF template with declaration data
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillPDFTemplate($template_path, $output_path, $declaration, $ca3_data, $company)
|
|
{
|
|
try {
|
|
// Check if we have a custom fillable PDF template
|
|
if (file_exists($template_path) && $this->isFillablePDF($template_path)) {
|
|
return $this->fillFillablePDF($template_path, $output_path, $declaration, $ca3_data, $company);
|
|
} else {
|
|
// Fall back to basic PDF generation
|
|
return $this->generateBasicPDF($output_path, $declaration, $ca3_data, $company);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->error = 'PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if PDF is fillable (has form fields)
|
|
*
|
|
* @param string $pdf_path PDF file path
|
|
* @return bool True if fillable
|
|
*/
|
|
private function isFillablePDF($pdf_path)
|
|
{
|
|
// Simple check - if it's our custom template, assume it's fillable
|
|
return strpos($pdf_path, 'ca3_custom_template.pdf') !== false;
|
|
}
|
|
|
|
/**
|
|
* Fill a fillable PDF template using pdftk
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillFillablePDF($template_path, $output_path, $declaration, $ca3_data, $company)
|
|
{
|
|
try {
|
|
// Check if pdftk is available
|
|
if (!$this->isPdftkAvailable()) {
|
|
// Fallback to manual approach if pdftk is not available
|
|
return $this->fillFillablePDFManual($template_path, $output_path, $declaration, $ca3_data, $company);
|
|
}
|
|
|
|
// Prepare field data mapping
|
|
$field_data = $this->prepareFieldData($declaration, $ca3_data, $company);
|
|
|
|
// Create FDF data file for pdftk
|
|
$fdf_file = $this->createFDFFile($field_data, $declaration);
|
|
|
|
// Use pdftk to fill the form
|
|
$pdftk_path = $this->getPdftkPath();
|
|
$command = "\"$pdftk_path\" \"$template_path\" fill_form \"$fdf_file\" output \"$output_path\"";
|
|
$result = shell_exec($command . ' 2>&1');
|
|
|
|
// Clean up temporary FDF file
|
|
if (file_exists($fdf_file)) {
|
|
unlink($fdf_file);
|
|
}
|
|
|
|
// Check if output file was created successfully
|
|
if (file_exists($output_path) && filesize($output_path) > 0) {
|
|
return true;
|
|
} else {
|
|
$this->error = 'pdftk failed to generate output: ' . $result;
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to fill PDF template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare field data for PDF form filling
|
|
*
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return array Field data mapping
|
|
*/
|
|
private function prepareFieldData($declaration, $ca3_data, $company)
|
|
{
|
|
$field_data = array();
|
|
|
|
// Get the actual company information from Dolibarr configuration
|
|
global $conf, $mysoc;
|
|
|
|
// Use Dolibarr's company configuration
|
|
$field_data['company_name'] = $mysoc->name;
|
|
$field_data['company_address'] = $mysoc->address;
|
|
$field_data['company_city'] = $mysoc->town;
|
|
$field_data['company_postal_code'] = $mysoc->zip;
|
|
|
|
// Debug: Log company information to see what's available
|
|
error_log("DeclarationTVA: Company data - name: " . $mysoc->name);
|
|
error_log("DeclarationTVA: Company data - idprof1: " . $mysoc->idprof1);
|
|
error_log("DeclarationTVA: Company data - idprof2: " . $mysoc->idprof2);
|
|
error_log("DeclarationTVA: Company data - idprof3: " . $mysoc->idprof3);
|
|
error_log("DeclarationTVA: Company data - idprof4: " . $mysoc->idprof4);
|
|
error_log("DeclarationTVA: Company data - tva_intra: " . $mysoc->tva_intra);
|
|
|
|
// Try different fields for SIRET - it could be in different idprof fields
|
|
$siret_value = !empty($mysoc->idprof2) ? $mysoc->idprof2 :
|
|
(!empty($mysoc->idprof1) ? $mysoc->idprof1 :
|
|
(!empty($mysoc->idprof3) ? $mysoc->idprof3 :
|
|
(!empty($mysoc->idprof4) ? $mysoc->idprof4 : '')));
|
|
|
|
// If no SIRET found, use a placeholder for testing
|
|
if (empty($siret_value)) {
|
|
$siret_value = 'SIRET_NOT_CONFIGURED';
|
|
error_log("DeclarationTVA: No SIRET found in company configuration. Please configure SIRET in Dolibarr company settings.");
|
|
}
|
|
|
|
$field_data['company_siret'] = $siret_value;
|
|
|
|
$field_data['company_vat_number'] = $mysoc->tva_intra; // VAT number
|
|
$field_data['declaration_period_start'] = dol_print_date($declaration->start_date, 'day');
|
|
$field_data['declaration_period_end'] = dol_print_date($declaration->end_date, 'day');
|
|
$field_data['declaration_number'] = $declaration->declaration_number;
|
|
|
|
// Section A: Opérations imposables
|
|
$field_data['A1_amount'] = $this->getCA3LineAmount($ca3_data, 'A1');
|
|
$field_data['A2_amount'] = $this->getCA3LineAmount($ca3_data, 'A2');
|
|
$field_data['A3_amount'] = $this->getCA3LineAmount($ca3_data, 'A3');
|
|
$field_data['A4_amount'] = $this->getCA3LineAmount($ca3_data, 'A4');
|
|
$field_data['A5_amount'] = $this->getCA3LineAmount($ca3_data, 'A5');
|
|
|
|
// Section A: Additional reference fields (E1-E6, F1-F2, F6-F8)
|
|
$field_data['E1_amount'] = $this->getCA3LineAmount($ca3_data, 'E1');
|
|
$field_data['E2_amount'] = $this->getCA3LineAmount($ca3_data, 'E2');
|
|
$field_data['E3_amount'] = $this->getCA3LineAmount($ca3_data, 'E3');
|
|
$field_data['E4_amount'] = $this->getCA3LineAmount($ca3_data, 'E4');
|
|
$field_data['E5_amount'] = $this->getCA3LineAmount($ca3_data, 'E5');
|
|
$field_data['E6_amount'] = $this->getCA3LineAmount($ca3_data, 'E6');
|
|
$field_data['F1_amount'] = $this->getCA3LineAmount($ca3_data, 'F1');
|
|
$field_data['F2_amount'] = $this->getCA3LineAmount($ca3_data, 'F2');
|
|
$field_data['F6_amount'] = $this->getCA3LineAmount($ca3_data, 'F6');
|
|
$field_data['F7_amount'] = $this->getCA3LineAmount($ca3_data, 'F7');
|
|
$field_data['F8_amount'] = $this->getCA3LineAmount($ca3_data, 'F8');
|
|
|
|
// Debug: Log the CA-3 data structure for troubleshooting
|
|
if (empty($ca3_data)) {
|
|
error_log("DeclarationTVA: CA-3 data is empty");
|
|
// Use test data for debugging
|
|
$ca3_data = array(
|
|
'A1' => array('vat_amount' => 1000.00),
|
|
'A2' => array('vat_amount' => 2000.00),
|
|
'08' => array('base_amount' => 5000.00, 'vat_amount' => 1000.00),
|
|
'09' => array('base_amount' => 10000.00, 'vat_amount' => 2000.00),
|
|
'20' => array('vat_amount' => 500.00),
|
|
'F1' => array('vat_amount' => 300.00),
|
|
'F2' => array('vat_amount' => 400.00)
|
|
);
|
|
error_log("DeclarationTVA: Using test data for debugging");
|
|
} else {
|
|
error_log("DeclarationTVA: CA-3 data structure: " . print_r($ca3_data, true));
|
|
}
|
|
|
|
// Debug: Log specific amount field values
|
|
error_log("DeclarationTVA: A1_amount will be: " . $this->getCA3LineAmount($ca3_data, 'A1'));
|
|
error_log("DeclarationTVA: B08_vat_amount will be: " . $this->getCA3LineAmount($ca3_data, '08', 'vat_amount'));
|
|
error_log("DeclarationTVA: B09_vat_amount will be: " . $this->getCA3LineAmount($ca3_data, '09', 'vat_amount'));
|
|
error_log("DeclarationTVA: total_vat_collected will be: " . $this->formatAmount($declaration->total_vat_collected));
|
|
|
|
// Section B: TVA due (Base + VAT columns)
|
|
$field_data['B08_base_amount'] = $this->getCA3LineAmount($ca3_data, '08', 'base_amount');
|
|
$field_data['B08_vat_amount'] = $this->getCA3LineAmount($ca3_data, '08', 'vat_amount');
|
|
$field_data['B09_base_amount'] = $this->getCA3LineAmount($ca3_data, '09', 'base_amount');
|
|
$field_data['B09_vat_amount'] = $this->getCA3LineAmount($ca3_data, '09', 'vat_amount');
|
|
$field_data['B9B_base_amount'] = $this->getCA3LineAmount($ca3_data, '9B', 'base_amount');
|
|
$field_data['B9B_vat_amount'] = $this->getCA3LineAmount($ca3_data, '9B', 'vat_amount');
|
|
$field_data['B17_amount'] = $this->getCA3LineAmount($ca3_data, '17');
|
|
|
|
// Section C: TVA déductible
|
|
$field_data['C20_amount'] = $this->getCA3LineAmount($ca3_data, '20');
|
|
$field_data['C21_amount'] = $this->getCA3LineAmount($ca3_data, '21');
|
|
$field_data['C22_amount'] = $this->getCA3LineAmount($ca3_data, '22');
|
|
|
|
// Section F: Intracom Acquisitions (already mapped above in Section A)
|
|
|
|
// Section D: Résultat (Calculated)
|
|
$field_data['D25_amount'] = $this->getCA3LineAmount($ca3_data, '25');
|
|
$field_data['D26_amount'] = $this->getCA3LineAmount($ca3_data, '26');
|
|
$field_data['DTD_amount'] = $this->getCA3LineAmount($ca3_data, 'TD');
|
|
$field_data['D28_amount'] = $this->getCA3LineAmount($ca3_data, '28');
|
|
$field_data['D29_amount'] = $this->getCA3LineAmount($ca3_data, '29');
|
|
|
|
// Subtotals
|
|
$field_data['subtotal_B16_amount'] = $this->getCA3LineAmount($ca3_data, '16');
|
|
$field_data['subtotal_C23_amount'] = $this->getCA3LineAmount($ca3_data, '23');
|
|
|
|
// Grand totals
|
|
$field_data['total_vat_collected'] = $this->formatAmount($declaration->total_vat_collected);
|
|
$field_data['total_vat_deductible'] = $this->formatAmount($declaration->total_vat_deductible);
|
|
$field_data['net_vat_due'] = $this->formatAmount($declaration->net_vat_due);
|
|
$field_data['vat_credit'] = $this->formatAmount($declaration->vat_credit);
|
|
|
|
return $field_data;
|
|
}
|
|
|
|
/**
|
|
* Format amount for PDF display
|
|
*
|
|
* @param float $amount Amount to format
|
|
* @return string Formatted amount
|
|
*/
|
|
private function formatAmount($amount)
|
|
{
|
|
return number_format($amount, 0, ',', ' ');
|
|
}
|
|
|
|
/**
|
|
* Fill a PDF form field by modifying PDF content
|
|
*
|
|
* @param string $pdf_content PDF content
|
|
* @param string $field_name Field name
|
|
* @param string $value Field value
|
|
* @return string Modified PDF content
|
|
*/
|
|
private function fillPDFFormField($pdf_content, $field_name, $value)
|
|
{
|
|
// Escape special characters in the value
|
|
$escaped_value = $this->escapePDFString($value);
|
|
|
|
// Look for the field in the PDF content and replace its value
|
|
// This is a simplified approach that works for many PDF types
|
|
|
|
// Pattern 1: Look for /T(field_name) and add /V(value) after it
|
|
$pattern1 = '/\/T\s*\(\s*' . preg_quote($field_name, '/') . '\s*\)/';
|
|
if (preg_match($pattern1, $pdf_content)) {
|
|
$pdf_content = preg_replace(
|
|
$pattern1,
|
|
'/T(' . $field_name . ') /V(' . $escaped_value . ')',
|
|
$pdf_content
|
|
);
|
|
}
|
|
|
|
// Pattern 2: Look for existing /V values and replace them
|
|
$pattern2 = '/\/T\s*\(\s*' . preg_quote($field_name, '/') . '\s*\).*?\/V\s*\([^)]*\)/';
|
|
if (preg_match($pattern2, $pdf_content)) {
|
|
$pdf_content = preg_replace(
|
|
$pattern2,
|
|
'/T(' . $field_name . ') /V(' . $escaped_value . ')',
|
|
$pdf_content
|
|
);
|
|
}
|
|
|
|
return $pdf_content;
|
|
}
|
|
|
|
/**
|
|
* Check if pdftk is available on the system
|
|
*
|
|
* @return bool True if pdftk is available
|
|
*/
|
|
private function isPdftkAvailable()
|
|
{
|
|
// Try multiple common locations for pdftk
|
|
$possible_paths = array(
|
|
'/usr/bin/pdftk',
|
|
'/usr/local/bin/pdftk',
|
|
'/bin/pdftk',
|
|
'/opt/pdftk/bin/pdftk',
|
|
'/usr/share/pdftk/bin/pdftk',
|
|
'/home/*/bin/pdftk',
|
|
'/home/*/.local/bin/pdftk'
|
|
);
|
|
|
|
foreach ($possible_paths as $path) {
|
|
if (strpos($path, '*') !== false) {
|
|
// Handle glob patterns
|
|
$glob_paths = glob($path);
|
|
foreach ($glob_paths as $glob_path) {
|
|
if (file_exists($glob_path) && is_executable($glob_path)) {
|
|
$test_result = shell_exec("$glob_path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (file_exists($path) && is_executable($path)) {
|
|
$test_result = shell_exec("$path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try 'which pdftk' in case it's in PATH
|
|
$result = shell_exec('which pdftk 2>/dev/null');
|
|
if (!empty(trim($result))) {
|
|
$test_result = shell_exec(trim($result) . ' --version 2>&1');
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the full path to pdftk executable
|
|
*
|
|
* @return string Path to pdftk executable
|
|
*/
|
|
private function getPdftkPath()
|
|
{
|
|
// Try multiple common locations for pdftk
|
|
$possible_paths = array(
|
|
'/usr/bin/pdftk',
|
|
'/usr/local/bin/pdftk',
|
|
'/bin/pdftk',
|
|
'/opt/pdftk/bin/pdftk',
|
|
'/usr/share/pdftk/bin/pdftk',
|
|
'/home/*/bin/pdftk',
|
|
'/home/*/.local/bin/pdftk'
|
|
);
|
|
|
|
foreach ($possible_paths as $path) {
|
|
if (strpos($path, '*') !== false) {
|
|
// Handle glob patterns
|
|
$glob_paths = glob($path);
|
|
foreach ($glob_paths as $glob_path) {
|
|
if (file_exists($glob_path) && is_executable($glob_path)) {
|
|
$test_result = shell_exec("$glob_path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return $glob_path;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (file_exists($path) && is_executable($path)) {
|
|
$test_result = shell_exec("$path --version 2>&1");
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return $path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: try 'which pdftk' in case it's in PATH
|
|
$result = shell_exec('which pdftk 2>/dev/null');
|
|
if (!empty(trim($result))) {
|
|
$test_result = shell_exec(trim($result) . ' --version 2>&1');
|
|
if (!empty($test_result) && strpos($test_result, 'pdftk') !== false) {
|
|
return trim($result);
|
|
}
|
|
}
|
|
|
|
// Default fallback
|
|
return 'pdftk';
|
|
}
|
|
|
|
/**
|
|
* Create FDF file for pdftk form filling
|
|
*
|
|
* @param array $field_data Field data
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @return string FDF file path
|
|
*/
|
|
private function createFDFFile($field_data, $declaration)
|
|
{
|
|
$fdf_file = sys_get_temp_dir() . '/ca3_fdf_' . $declaration->declaration_number . '_' . uniqid() . '.fdf';
|
|
|
|
$fdf_content = "%FDF-1.2\n";
|
|
$fdf_content .= "1 0 obj\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/FDF\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/Fields [\n";
|
|
|
|
foreach ($field_data as $field_name => $value) {
|
|
if (!empty($value)) {
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/T (" . $field_name . ")\n";
|
|
$fdf_content .= "/V (" . $this->escapeFDFString($value) . ")\n";
|
|
|
|
// Force Courier New 9pt for official documents
|
|
$fdf_content .= "/Ff 0\n"; // Field flags (0 = no special flags)
|
|
$fdf_content .= "/F 9\n"; // Font size 9pt
|
|
$fdf_content .= "/DA (/Cour 9 Tf 0 g)\n"; // Courier 9pt, black
|
|
$fdf_content .= "/Q 0\n"; // Left alignment
|
|
$fdf_content .= ">>\n";
|
|
}
|
|
}
|
|
|
|
$fdf_content .= "]\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= "endobj\n";
|
|
$fdf_content .= "trailer\n";
|
|
$fdf_content .= "<<\n";
|
|
$fdf_content .= "/Root 1 0 R\n";
|
|
$fdf_content .= ">>\n";
|
|
$fdf_content .= "%%EOF\n";
|
|
|
|
file_put_contents($fdf_file, $fdf_content);
|
|
return $fdf_file;
|
|
}
|
|
|
|
/**
|
|
* Escape string for FDF format
|
|
*
|
|
* @param string $value Value to escape
|
|
* @return string Escaped value
|
|
*/
|
|
private function escapeFDFString($value)
|
|
{
|
|
// Escape special FDF characters
|
|
$value = str_replace('\\', '\\\\', $value);
|
|
$value = str_replace('(', '\\(', $value);
|
|
$value = str_replace(')', '\\)', $value);
|
|
return $value;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Manual PDF filling fallback (when pdftk is not available)
|
|
*
|
|
* @param string $template_path Template file path
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function fillFillablePDFManual($template_path, $output_path, $declaration, $ca3_data, $company)
|
|
{
|
|
try {
|
|
// Simple approach: Just copy the template and create a data file
|
|
if (copy($template_path, $output_path)) {
|
|
// Create a data file with all the values for manual filling
|
|
$data_file = dirname($output_path) . '/ca3_data_' . $declaration->declaration_number . '.txt';
|
|
$field_data = $this->prepareFieldData($declaration, $ca3_data, $company);
|
|
|
|
$data_content = "CA-3 Declaration Data (Manual Filling Required)\n";
|
|
$data_content .= "============================================\n\n";
|
|
$data_content .= "Note: pdftk is not installed. Please install pdftk for automatic form filling.\n";
|
|
$data_content .= "See installation guide: /custom/declarationtva/docs/PDFTK_INSTALLATION.md\n\n";
|
|
|
|
$data_content .= "Declaration: " . $declaration->declaration_number . "\n";
|
|
$data_content .= "Period: " . dol_print_date($declaration->start_date, 'day') . " - " . dol_print_date($declaration->end_date, 'day') . "\n\n";
|
|
|
|
$data_content .= "Company Information:\n";
|
|
$data_content .= "-------------------\n";
|
|
$data_content .= "company_name: " . $field_data['company_name'] . "\n";
|
|
$data_content .= "company_address: " . $field_data['company_address'] . "\n";
|
|
$data_content .= "company_city: " . $field_data['company_city'] . "\n";
|
|
$data_content .= "company_postal_code: " . $field_data['company_postal_code'] . "\n";
|
|
$data_content .= "company_siret: " . $field_data['company_siret'] . "\n\n";
|
|
|
|
$data_content .= "Declaration Information:\n";
|
|
$data_content .= "------------------------\n";
|
|
$data_content .= "declaration_period_start: " . $field_data['declaration_period_start'] . "\n";
|
|
$data_content .= "declaration_period_end: " . $field_data['declaration_period_end'] . "\n";
|
|
$data_content .= "declaration_number: " . $field_data['declaration_number'] . "\n\n";
|
|
|
|
$data_content .= "Section A - Opérations imposables:\n";
|
|
$data_content .= "----------------------------------\n";
|
|
$data_content .= "A1_amount: " . $field_data['A1_amount'] . "\n";
|
|
$data_content .= "A2_amount: " . $field_data['A2_amount'] . "\n";
|
|
$data_content .= "A3_amount: " . $field_data['A3_amount'] . "\n";
|
|
$data_content .= "A4_amount: " . $field_data['A4_amount'] . "\n";
|
|
$data_content .= "A5_amount: " . $field_data['A5_amount'] . "\n";
|
|
$data_content .= "E1_amount: " . $field_data['E1_amount'] . "\n";
|
|
$data_content .= "E2_amount: " . $field_data['E2_amount'] . "\n";
|
|
$data_content .= "E3_amount: " . $field_data['E3_amount'] . "\n";
|
|
$data_content .= "E4_amount: " . $field_data['E4_amount'] . "\n";
|
|
$data_content .= "E5_amount: " . $field_data['E5_amount'] . "\n";
|
|
$data_content .= "E6_amount: " . $field_data['E6_amount'] . "\n";
|
|
$data_content .= "F1_amount: " . $field_data['F1_amount'] . "\n";
|
|
$data_content .= "F2_amount: " . $field_data['F2_amount'] . "\n";
|
|
$data_content .= "F6_amount: " . $field_data['F6_amount'] . "\n";
|
|
$data_content .= "F7_amount: " . $field_data['F7_amount'] . "\n";
|
|
$data_content .= "F8_amount: " . $field_data['F8_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section B - TVA due:\n";
|
|
$data_content .= "--------------------\n";
|
|
$data_content .= "B08_base_amount: " . $field_data['B08_base_amount'] . "\n";
|
|
$data_content .= "B08_vat_amount: " . $field_data['B08_vat_amount'] . "\n";
|
|
$data_content .= "B09_base_amount: " . $field_data['B09_base_amount'] . "\n";
|
|
$data_content .= "B09_vat_amount: " . $field_data['B09_vat_amount'] . "\n";
|
|
$data_content .= "B9B_base_amount: " . $field_data['B9B_base_amount'] . "\n";
|
|
$data_content .= "B9B_vat_amount: " . $field_data['B9B_vat_amount'] . "\n";
|
|
$data_content .= "B17_amount: " . $field_data['B17_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section C - TVA déductible:\n";
|
|
$data_content .= "----------------------------\n";
|
|
$data_content .= "C20_amount: " . $field_data['C20_amount'] . "\n";
|
|
$data_content .= "C21_amount: " . $field_data['C21_amount'] . "\n";
|
|
$data_content .= "C22_amount: " . $field_data['C22_amount'] . "\n\n";
|
|
|
|
$data_content .= "Section D - Résultat:\n";
|
|
$data_content .= "---------------------\n";
|
|
$data_content .= "D25_amount: " . $field_data['D25_amount'] . "\n";
|
|
$data_content .= "D26_amount: " . $field_data['D26_amount'] . "\n";
|
|
$data_content .= "DTD_amount: " . $field_data['DTD_amount'] . "\n";
|
|
$data_content .= "D28_amount: " . $field_data['D28_amount'] . "\n";
|
|
$data_content .= "D29_amount: " . $field_data['D29_amount'] . "\n\n";
|
|
|
|
$data_content .= "Totals:\n";
|
|
$data_content .= "-------\n";
|
|
$data_content .= "total_vat_collected: " . $field_data['total_vat_collected'] . "\n";
|
|
$data_content .= "total_vat_deductible: " . $field_data['total_vat_deductible'] . "\n";
|
|
$data_content .= "net_vat_due: " . $field_data['net_vat_due'] . "\n";
|
|
$data_content .= "vat_credit: " . $field_data['vat_credit'] . "\n";
|
|
|
|
file_put_contents($data_file, $data_content);
|
|
|
|
return true;
|
|
} else {
|
|
$this->error = 'Failed to copy template file';
|
|
return false;
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Failed to fill PDF template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate basic PDF (fallback method)
|
|
*
|
|
* @param string $output_path Output file path
|
|
* @param DeclarationTVA $declaration Declaration object
|
|
* @param array $ca3_data CA-3 line data
|
|
* @param Societe $company Company object
|
|
* @return bool Success
|
|
*/
|
|
private function generateBasicPDF($output_path, $declaration, $ca3_data, $company)
|
|
{
|
|
try {
|
|
// Create a new PDF document
|
|
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
|
|
|
// Set document information
|
|
$pdf->SetCreator('DeclarationTVA Module');
|
|
$pdf->SetAuthor($company->name);
|
|
$pdf->SetTitle('CA-3 Declaration ' . $declaration->declaration_number);
|
|
$pdf->SetSubject('French VAT Declaration');
|
|
|
|
// Set margins
|
|
$pdf->SetMargins(15, 15, 15);
|
|
$pdf->SetHeaderMargin(5);
|
|
$pdf->SetFooterMargin(10);
|
|
|
|
// Add a page
|
|
$pdf->AddPage();
|
|
|
|
// Add title
|
|
$pdf->SetFont('helvetica', 'B', 16);
|
|
$pdf->Cell(0, 10, 'Déclaration TVA CA-3', 0, 1, 'C');
|
|
$pdf->Ln(10);
|
|
|
|
// Add declaration information
|
|
$pdf->SetFont('helvetica', '', 12);
|
|
$pdf->Cell(0, 8, 'Numéro de déclaration: ' . $declaration->declaration_number, 0, 1);
|
|
$pdf->Cell(0, 8, 'Période: ' . dol_print_date($declaration->start_date, 'day') . ' - ' . dol_print_date($declaration->end_date, 'day'), 0, 1);
|
|
$pdf->Cell(0, 8, 'Statut: ' . $declaration->status, 0, 1);
|
|
$pdf->Ln(10);
|
|
|
|
// Add CA-3 sections
|
|
$this->addCA3Section($pdf, 'A. Opérations imposables', $ca3_data, array('A1', 'A2', 'A3', 'A4', 'A5'));
|
|
$this->addCA3Section($pdf, 'B. TVA due', $ca3_data, array('08', '09', '9B', '17'));
|
|
$this->addCA3Section($pdf, 'C. TVA déductible', $ca3_data, array('20', '21', '22'));
|
|
$this->addCA3Section($pdf, 'D. Résultat', $ca3_data, array('25', '26', 'TD', '28', '29'));
|
|
|
|
// Add totals
|
|
$pdf->Ln(10);
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA COLLECTÉE: ' . price($declaration->total_vat_collected, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TOTAL TVA DÉDUCTIBLE: ' . price($declaration->total_vat_deductible, 0, '', 1, 0), 0, 1);
|
|
$pdf->Cell(0, 8, 'TVA NETTE DUE: ' . price($declaration->net_vat_due, 0, '', 1, 0), 0, 1);
|
|
|
|
if ($declaration->vat_credit > 0) {
|
|
$pdf->Cell(0, 8, 'CRÉDIT DE TVA: ' . price($declaration->vat_credit, 0, '', 1, 0), 0, 1);
|
|
}
|
|
|
|
// Output PDF
|
|
$pdf->Output($output_path, 'F');
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Basic PDF generation failed: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add CA-3 section to PDF
|
|
*
|
|
* @param TCPDF $pdf PDF object
|
|
* @param string $section_title Section title
|
|
* @param array $ca3_data CA-3 data
|
|
* @param array $lines Lines to include
|
|
*/
|
|
private function addCA3Section($pdf, $section_title, $ca3_data, $lines)
|
|
{
|
|
$pdf->SetFont('helvetica', 'B', 12);
|
|
$pdf->Cell(0, 8, $section_title, 0, 1);
|
|
|
|
$pdf->SetFont('helvetica', '', 10);
|
|
foreach ($lines as $line) {
|
|
if (isset($ca3_data[$line])) {
|
|
$data = $ca3_data[$line];
|
|
$amount = isset($data['vat_amount']) ? $data['vat_amount'] : 0;
|
|
$pdf->Cell(20, 6, $line, 1, 0, 'C');
|
|
$pdf->Cell(100, 6, $data['line_label'], 1, 0);
|
|
$pdf->Cell(30, 6, price($amount, 0, '', 1, 0), 1, 1, 'R');
|
|
}
|
|
}
|
|
$pdf->Ln(5);
|
|
}
|
|
|
|
/**
|
|
* Upload custom template
|
|
*
|
|
* @param array $file Uploaded file array
|
|
* @return bool Success
|
|
*/
|
|
public function uploadCustomTemplate($file)
|
|
{
|
|
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
|
$this->error = 'No file uploaded';
|
|
return false;
|
|
}
|
|
|
|
// Validate file type
|
|
$file_info = pathinfo($file['name']);
|
|
if (strtolower($file_info['extension']) !== 'pdf') {
|
|
$this->error = 'Only PDF files are allowed';
|
|
return false;
|
|
}
|
|
|
|
// Validate file size (max 10MB)
|
|
if ($file['size'] > 10 * 1024 * 1024) {
|
|
$this->error = 'File too large (max 10MB)';
|
|
return false;
|
|
}
|
|
|
|
// Ensure template directory exists
|
|
if (!is_dir($this->template_path)) {
|
|
if (!dol_mkdir($this->template_path)) {
|
|
$this->error = 'Failed to create template directory';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check directory permissions
|
|
if (!is_writable($this->template_path)) {
|
|
$this->error = 'Template directory is not writable';
|
|
return false;
|
|
}
|
|
|
|
// Move uploaded file
|
|
$target_path = $this->template_path . 'ca3_custom_template.pdf';
|
|
|
|
// Remove existing custom template if it exists
|
|
if (file_exists($target_path)) {
|
|
unlink($target_path);
|
|
}
|
|
|
|
if (move_uploaded_file($file['tmp_name'], $target_path)) {
|
|
// Verify the file was saved correctly
|
|
if (file_exists($target_path) && filesize($target_path) > 0) {
|
|
return true;
|
|
} else {
|
|
$this->error = 'Template file was not saved correctly';
|
|
return false;
|
|
}
|
|
} else {
|
|
$last_error = error_get_last();
|
|
$this->error = 'Failed to save template: ' . ($last_error ? $last_error['message'] : 'Unknown error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get template information
|
|
*
|
|
* @return array Template information
|
|
*/
|
|
public function getTemplateInfo()
|
|
{
|
|
$info = array(
|
|
'version' => $this->template_version,
|
|
'document' => $this->template_document,
|
|
'official_number' => $this->template_document . '*' . $this->template_version,
|
|
'custom_template' => false,
|
|
'template_path' => ''
|
|
);
|
|
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
$info['custom_template'] = true;
|
|
$info['template_path'] = $custom_template;
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Reset to default template
|
|
*
|
|
* @return bool Success
|
|
*/
|
|
public function resetToDefaultTemplate()
|
|
{
|
|
$custom_template = $this->template_path . 'ca3_custom_template.pdf';
|
|
if (file_exists($custom_template)) {
|
|
return unlink($custom_template);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check for template updates from Gitea
|
|
*
|
|
* @return array Update information
|
|
*/
|
|
public function checkTemplateUpdates()
|
|
{
|
|
$update_info = array(
|
|
'update_available' => false,
|
|
'current_version' => $this->template_version,
|
|
'latest_version' => $this->template_version,
|
|
'download_url' => '',
|
|
'release_date' => '',
|
|
'error' => ''
|
|
);
|
|
|
|
try {
|
|
// Fetch manifest from Gitea
|
|
$manifest_content = file_get_contents($this->manifest_url);
|
|
if ($manifest_content === false) {
|
|
$update_info['error'] = 'Failed to fetch manifest from Gitea';
|
|
return $update_info;
|
|
}
|
|
|
|
$manifest = json_decode($manifest_content, true);
|
|
if (!$manifest || !isset($manifest['templates']['ca3'])) {
|
|
$update_info['error'] = 'Invalid manifest format';
|
|
return $update_info;
|
|
}
|
|
|
|
$template_info = $manifest['templates']['ca3'];
|
|
$latest_version = $template_info['current_version'];
|
|
$current_version = $this->template_version;
|
|
|
|
// Check if update is available
|
|
if (version_compare($latest_version, $current_version, '>')) {
|
|
$update_info['update_available'] = true;
|
|
$update_info['latest_version'] = $latest_version;
|
|
|
|
if (isset($template_info['releases'][$latest_version])) {
|
|
$release_info = $template_info['releases'][$latest_version];
|
|
$update_info['download_url'] = $release_info['download_url'];
|
|
$update_info['release_date'] = $release_info['release_date'];
|
|
}
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$update_info['error'] = 'Error checking updates: ' . $e->getMessage();
|
|
}
|
|
|
|
return $update_info;
|
|
}
|
|
|
|
/**
|
|
* Download and install template update
|
|
*
|
|
* @param string $version Version to download
|
|
* @param string $download_url Download URL
|
|
* @return bool Success
|
|
*/
|
|
public function downloadTemplateUpdate($version, $download_url)
|
|
{
|
|
try {
|
|
// Ensure template directory exists
|
|
if (!is_dir($this->template_path)) {
|
|
dol_mkdir($this->template_path);
|
|
}
|
|
|
|
// Download template
|
|
$template_content = file_get_contents($download_url);
|
|
if ($template_content === false) {
|
|
$this->error = 'Failed to download template from Gitea';
|
|
return false;
|
|
}
|
|
|
|
// Save as official template
|
|
$template_file = $this->template_path . 'ca3_official_template.pdf';
|
|
if (file_put_contents($template_file, $template_content) === false) {
|
|
$this->error = 'Failed to save downloaded template';
|
|
return false;
|
|
}
|
|
|
|
// Update version info
|
|
$this->template_version = $version;
|
|
|
|
return true;
|
|
|
|
} catch (Exception $e) {
|
|
$this->error = 'Error downloading template: ' . $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get template update status
|
|
*
|
|
* @return array Status information
|
|
*/
|
|
public function getTemplateUpdateStatus()
|
|
{
|
|
$status = array(
|
|
'current_version' => $this->template_version,
|
|
'update_available' => false,
|
|
'latest_version' => $this->template_version,
|
|
'last_check' => '',
|
|
'error' => ''
|
|
);
|
|
|
|
// Check for updates
|
|
$update_info = $this->checkTemplateUpdates();
|
|
|
|
if ($update_info['update_available']) {
|
|
$status['update_available'] = true;
|
|
$status['latest_version'] = $update_info['latest_version'];
|
|
}
|
|
|
|
if (!empty($update_info['error'])) {
|
|
$status['error'] = $update_info['error'];
|
|
}
|
|
|
|
// Store last check time
|
|
$status['last_check'] = date('Y-m-d H:i:s');
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Auto-update template if available
|
|
*
|
|
* @return bool Success
|
|
*/
|
|
public function autoUpdateTemplate()
|
|
{
|
|
$update_info = $this->checkTemplateUpdates();
|
|
|
|
if ($update_info['update_available'] && !empty($update_info['download_url'])) {
|
|
return $this->downloadTemplateUpdate(
|
|
$update_info['latest_version'],
|
|
$update_info['download_url']
|
|
);
|
|
}
|
|
|
|
return true; // No update needed
|
|
}
|
|
|
|
/**
|
|
* Get CA-3 line amount from the data array
|
|
*
|
|
* @param array $ca3_data CA-3 data array
|
|
* @param string $ca3_line Line identifier (e.g., 'A1', '08', '17')
|
|
* @param string $amount_type Amount type ('vat_amount' or 'base_amount')
|
|
* @return string Formatted amount
|
|
*/
|
|
private function getCA3LineAmount($ca3_data, $ca3_line, $amount_type = 'vat_amount')
|
|
{
|
|
if (empty($ca3_data) || !is_array($ca3_data)) {
|
|
return '0,00';
|
|
}
|
|
|
|
// Search through the array for the matching ca3_line
|
|
foreach ($ca3_data as $line_data) {
|
|
if (isset($line_data['ca3_line']) && $line_data['ca3_line'] == $ca3_line) {
|
|
$amount = isset($line_data[$amount_type]) ? $line_data[$amount_type] : 0;
|
|
return $this->formatAmount($amount);
|
|
}
|
|
}
|
|
|
|
return '0,00';
|
|
}
|
|
}
|