DeclarationTVA/core/class/declarationtva.class.php
Frank Cools 038cd3ed67 Remove direct SQL approach and use proper Dolibarr error handling
- Remove direct SQL insert fallback that bypasses Dolibarr logic
- Use proper result checking: if (result > 0) for success
- Log actual entry IDs when successfully created
- Use bookkeeping->error for proper error messages
- Maintain Dolibarr's internal validation and business logic
2025-10-08 22:13:34 +02:00

1973 lines
76 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* DeclarationTVA Class
* French CA-3 VAT Declaration Module for Dolibarr
* MVP Version - Phase 1
*/
class DeclarationTVA
{
/**
* @var DoliDB Database handler
*/
public $db;
/**
* @var int Entity ID
*/
public $entity;
/**
* @var string Error message
*/
public $error;
/**
* @var int Declaration ID
*/
public $rowid;
/**
* @var int Period ID
*/
public $period_id;
/**
* @var string Declaration number
*/
public $declaration_number;
/**
* @var string Declaration name
*/
public $declaration_name;
/**
* @var string Start date
*/
public $start_date;
/**
* @var string End date
*/
public $end_date;
/**
* @var string Status
*/
public $status;
/**
* Constructor
*
* @param DoliDB $db Database handler
* @param int $entity Entity ID
*/
public function __construct($db, $entity = 1)
{
$this->db = $db;
$this->entity = $entity;
}
/**
* Fetch declaration by ID
*
* @param int $id Declaration ID
* @return int 1 if found, 0 if not found, -1 if error
*/
public function fetch($id)
{
$sql = "SELECT * FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if ($result) {
$obj = $this->db->fetch_object($result);
if ($obj) {
$this->rowid = $obj->rowid;
$this->period_id = $obj->period_id;
$this->declaration_number = $obj->declaration_number;
$this->declaration_name = $obj->declaration_name;
$this->start_date = $obj->start_date;
$this->end_date = $obj->end_date;
$this->status = $obj->status;
$this->created_date = $obj->created_date;
return 1;
}
}
return 0;
}
/**
* Create a new declaration for a period
*
* @param int $period_id Period ID
* @return int Declaration ID or -1 if error
*/
public function createDeclaration($period_id)
{
global $user;
$this->db->begin();
// Get period information
$period = $this->getPeriod($period_id);
if (!$period) {
$this->error = "Period not found";
$this->db->rollback();
return -1;
}
// Generate declaration number
$declaration_number = $this->generateDeclarationNumber($period);
// Create declaration record
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_declarations
(entity, period_id, declaration_number, status, created_date)
VALUES (" . $this->entity . ", " . $period_id . ", '" . $declaration_number . "', 'draft', NOW())";
$result = $this->db->query($sql);
if (!$result) {
$this->error = "Error creating declaration: " . $this->db->lasterror();
$this->db->rollback();
return -1;
}
$declaration_id = $this->db->last_insert_id(MAIN_DB_PREFIX . "declarationtva_declarations");
// Calculate CA-3 amounts
$this->calculateCA3Amounts($declaration_id, $period);
$this->db->commit();
return $declaration_id;
}
/**
* Create a new declaration with specific dates
*
* @param string $start_date Start date (YYYY-MM-DD)
* @param string $end_date End date (YYYY-MM-DD)
* @param string $declaration_name Declaration name
* @return int Declaration ID or -1 if error
*/
public function createDeclarationWithDates($start_date, $end_date, $declaration_name = '')
{
global $user;
$this->db->begin();
// Generate declaration number
$declaration_number = $this->generateDeclarationNumberFromDates($start_date, $end_date);
// Create declaration record
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_declarations
(entity, period_id, declaration_number, declaration_name, start_date, end_date, status, created_date)
VALUES (" . $this->entity . ", 0, '" . $declaration_number . "', '" . $this->db->escape($declaration_name) . "', '" . $start_date . "', '" . $end_date . "', 'draft', NOW())";
$result = $this->db->query($sql);
if (!$result) {
$this->error = "Error creating declaration: " . $this->db->lasterror();
$this->db->rollback();
return -1;
}
$declaration_id = $this->db->last_insert_id(MAIN_DB_PREFIX . "declarationtva_declarations");
// Calculate CA-3 amounts
$period = array('start_date' => $start_date, 'end_date' => $end_date);
$this->calculateCA3Amounts($declaration_id, $period);
$this->db->commit();
return $declaration_id;
}
/**
* Calculate CA-3 amounts for a declaration
*
* @param int $declaration_id Declaration ID
* @param array $period Period information
* @return bool Success
*/
public function calculateCA3Amounts($declaration_id, $period)
{
// Get account mappings grouped by CA-3 line
$mappings = $this->getAccountMappings();
// Group mappings by CA-3 line
$grouped_mappings = array();
foreach ($mappings as $mapping) {
$ca3_line = $mapping['ca3_line'];
if (!isset($grouped_mappings[$ca3_line])) {
$grouped_mappings[$ca3_line] = array();
}
$grouped_mappings[$ca3_line][] = $mapping;
}
// Define all CA-3 lines that should be processed
$all_ca3_lines = array('A1', 'A2', 'A3', 'A4', 'A5', 'E1', 'E2', 'E3', 'E4', 'E5', 'E6', 'F1', 'F2', 'F6', 'F7', 'F8', '08', '09', '9B', '17', '18', '19', '20', '21', '22', '25', '26', '28', '29');
$total_vat_collected = 0;
$total_vat_deductible = 0;
// Process ALL CA-3 lines (even those without mappings)
foreach ($all_ca3_lines as $ca3_line) {
$line_total_base = 0;
$line_total_vat = 0;
$line_total_amount = 0;
$account_labels = array();
// Special handling for lines 08, 09, 9B (need separate base and VAT accounts)
if (in_array($ca3_line, array('08', '09', '9B'))) {
// Get base accounts (sales)
$base_mappings = isset($grouped_mappings[$ca3_line . '_BASE']) ? $grouped_mappings[$ca3_line . '_BASE'] : array();
foreach ($base_mappings as $mapping) {
$amounts = $this->getAccountAmounts($mapping['account_code'], $period['start_date'], $period['end_date']);
$line_total_base += $amounts['base_amount'];
$account_labels[] = $mapping['account_label'] . ' (base)';
}
// Get VAT accounts
$vat_mappings = isset($grouped_mappings[$ca3_line . '_VAT']) ? $grouped_mappings[$ca3_line . '_VAT'] : array();
foreach ($vat_mappings as $mapping) {
$amounts = $this->getAccountAmounts($mapping['account_code'], $period['start_date'], $period['end_date']);
$line_total_vat += $amounts['vat_amount'];
$account_labels[] = $mapping['account_label'] . ' (VAT)';
}
$line_total_amount = $line_total_base + $line_total_vat;
} else {
// Normal processing for other lines
if (isset($grouped_mappings[$ca3_line])) {
foreach ($grouped_mappings[$ca3_line] as $mapping) {
$amounts = $this->getAccountAmounts($mapping['account_code'], $period['start_date'], $period['end_date']);
$line_total_base += $amounts['base_amount'];
$line_total_vat += $amounts['vat_amount'];
$line_total_amount += $amounts['total_amount'];
$account_labels[] = $mapping['account_label'];
}
}
}
// Create CA-3 line record with summed amounts (even if zero)
$combined_amounts = array(
'base_amount' => $line_total_base,
'vat_amount' => $line_total_vat,
'total_amount' => $line_total_amount
);
$combined_label = !empty($account_labels) ? implode(', ', $account_labels) : 'No accounts mapped';
// Truncate label if too long to prevent database issues (keep some buffer)
if (strlen($combined_label) > 1000) {
$combined_label = substr($combined_label, 0, 997) . '...';
}
$this->createCA3Line($declaration_id, $ca3_line, $combined_label, $combined_amounts);
// Update totals - only VAT amounts from sections B and C
if (in_array($ca3_line, ['08', '09', '9B'])) {
// Section B: VAT amounts only
$total_vat_collected += $line_total_vat;
} elseif (in_array($ca3_line, ['19', '20', '21', '22'])) {
// Section B sub-section: VAT deductible amounts
$total_vat_deductible += $line_total_vat;
}
}
// Calculate line 16: Subtotal of lines 08, 09, 9B (for user reference)
$this->calculateLine16($declaration_id);
// Calculate line 23: Subtotal of lines 20, 21, 22 (for user reference)
$this->calculateLine23($declaration_id);
// Calculate D-section result lines (25, 26, 28, 29)
$this->calculateDSectionLines($declaration_id, $total_vat_collected, $total_vat_deductible);
// Calculate net amounts
$net_vat_due = $total_vat_collected - $total_vat_deductible;
$vat_credit = $net_vat_due < 0 ? abs($net_vat_due) : 0;
$net_vat_due = $net_vat_due > 0 ? $net_vat_due : 0;
// Update declaration totals
$this->updateDeclarationTotals($declaration_id, $total_vat_collected, $total_vat_deductible, $net_vat_due, $vat_credit);
return true;
}
/**
* Calculate line 16: Subtotal of lines 08, 09, 9B (for user reference)
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
private function calculateLine16($declaration_id)
{
// Get the amounts from lines 08, 09, 9B
$sql = "SELECT ca3_line, vat_amount FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . (int)$declaration_id . "
AND ca3_line IN ('08', '09', '9B')";
$result = $this->db->query($sql);
$line_16_total = 0;
$account_labels = array();
if ($result) {
while ($obj = $this->db->fetch_object($result)) {
$line_16_total += $obj->vat_amount;
$account_labels[] = "Line " . $obj->ca3_line . ": " . number_format($obj->vat_amount, 2);
}
}
// Create line 16 record
$this->createCA3Line($declaration_id, '16', 'Subtotal: ' . implode(', ', $account_labels), array(
'base_amount' => 0,
'vat_amount' => $line_16_total,
'total_amount' => $line_16_total
));
return true;
}
/**
* Round VAT amount to whole number and format with original amount in brackets
*
* @param float $amount Original amount
* @return array Array with 'rounded' and 'formatted' values
*/
private function roundVATAmount($amount)
{
$rounded = round($amount);
$formatted = $rounded;
if ($rounded != $amount) {
$formatted = $rounded . ' (' . number_format($amount, 2) . ')';
}
return array(
'rounded' => $rounded,
'formatted' => $formatted
);
}
/**
* Calculate line 23: Subtotal of lines 20, 21, 22 (for user reference)
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
private function calculateLine23($declaration_id)
{
// Get the amounts from lines 20, 21, 22
$sql = "SELECT ca3_line, vat_amount FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . (int)$declaration_id . "
AND ca3_line IN ('20', '21', '22')";
$result = $this->db->query($sql);
$line_23_total = 0;
$account_labels = array();
if ($result) {
while ($obj = $this->db->fetch_object($result)) {
$line_23_total += $obj->vat_amount;
$account_labels[] = "Line " . $obj->ca3_line . ": " . number_format($obj->vat_amount, 2);
}
}
// Create line 23 record
$this->createCA3Line($declaration_id, '23', 'Subtotal: ' . implode(', ', $account_labels), array(
'base_amount' => 0,
'vat_amount' => $line_23_total,
'total_amount' => $line_23_total
));
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)
*
* @param int $declaration_id Declaration ID
* @param float $total_vat_collected Total VAT collected
* @param float $total_vat_deductible Total VAT deductible
* @return bool Success
*/
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: 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', 'Crédit de TVA (ligne 23 - ligne16)', array(
'base_amount' => 0,
'vat_amount' => 0,
'total_amount' => 0
));
}
// Get VAT refund threshold configuration
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$threshold_config = $config->getVATRefundThresholdConfiguration();
$refund_threshold = $threshold_config['refund_threshold'];
$threshold_enabled = $threshold_config['refund_threshold_enabled'];
// Line 26: Crédit de TVA à rembourser (≥ threshold)
if ($line_25_amount > 0 && (!$threshold_enabled || $line_25_amount >= $refund_threshold)) {
$this->createCA3Line($declaration_id, '26', 'Crédit de TVA à rembourser (ligne 25 ligne 26)', array(
'base_amount' => 0,
'vat_amount' => $line_25_amount,
'total_amount' => $line_25_amount
));
} else {
$this->createCA3Line($declaration_id, '26', 'Crédit de TVA à rembourser (ligne 25 ligne 26)', array(
'base_amount' => 0,
'vat_amount' => 0,
'total_amount' => 0
));
}
// Line 27: Crédit de TVA à reporter (< threshold)
$line_27_amount = $line_25_amount; // Same value as line 25
if ($line_25_amount > 0 && $threshold_enabled && $line_25_amount < $refund_threshold) {
$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 (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 (ligne 16 ligne 23)', array(
'base_amount' => 0,
'vat_amount' => 0,
'total_amount' => 0
));
}
// 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' => $line_28_amount,
'total_amount' => $line_28_amount
));
} else {
$this->createCA3Line($declaration_id, '28', 'TVA nette due (ligne TD ligne X5)', array(
'base_amount' => 0,
'vat_amount' => 0,
'total_amount' => 0
));
}
// 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' => $line_32_amount,
'total_amount' => $line_32_amount
));
} else {
$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
));
}
return true;
}
/**
* Get account amounts for a period
*
* @param string $account_code Account code
* @param string $start_date Start date
* @param string $end_date End date
* @return array Amounts (base_amount, vat_amount, total_amount)
*/
public function getAccountAmounts($account_code, $start_date, $end_date)
{
// Use the correct Dolibarr accounting table structure
$possible_queries = array(
// Correct table and column names based on your Dolibarr structure
"SELECT SUM(debit) as total_debit, SUM(credit) as total_credit
FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping
WHERE numero_compte = '" . $this->db->escape($account_code) . "'
AND doc_date >= '" . $this->db->escape($start_date) . "'
AND doc_date <= '" . $this->db->escape($end_date) . "'
AND entity = " . $this->entity,
// Without entity filter (in case entity is not set correctly)
"SELECT SUM(debit) as total_debit, SUM(credit) as total_credit
FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping
WHERE numero_compte = '" . $this->db->escape($account_code) . "'
AND doc_date >= '" . $this->db->escape($start_date) . "'
AND doc_date <= '" . $this->db->escape($end_date) . "'"
);
foreach ($possible_queries as $sql) {
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
$obj = $this->db->fetch_object($result);
// For VAT accounts, we need to use the credit side (VAT collected) or debit side (VAT paid)
// If debit = credit, the account is balanced, so we use the credit amount (VAT collected)
if ($obj->total_debit == $obj->total_credit) {
// Account is balanced, use the credit amount (VAT collected)
$total_amount = $obj->total_credit;
} else {
// Account has imbalance, use the absolute difference
$total_amount = abs($obj->total_debit - $obj->total_credit);
}
return array(
'base_amount' => $total_amount,
'vat_amount' => $total_amount,
'total_amount' => $total_amount
);
}
}
return array('base_amount' => 0, 'vat_amount' => 0, 'total_amount' => 0);
}
/**
* Create CA-3 line record
*
* @param int $declaration_id Declaration ID
* @param string $ca3_line CA-3 line code
* @param string $line_label Line label
* @param array $amounts Amounts array
* @return bool Success
*/
public function createCA3Line($declaration_id, $ca3_line, $line_label, $amounts)
{
// Get amount calculation configuration
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$calc_config = $config->getAmountCalculationConfiguration();
// Apply calculation method (round or truncate)
if ($calc_config['amount_calculation_method'] == 'truncate') {
$rounded_base = floor($amounts['base_amount']);
$rounded_vat = floor($amounts['vat_amount']);
$rounded_total = floor($amounts['total_amount']);
} else {
// Default to rounding
$rounded_base = round($amounts['base_amount']);
$rounded_vat = round($amounts['vat_amount']);
$rounded_total = round($amounts['total_amount']);
}
// Store original amounts in the line_label for now (we'll need to modify the database schema later)
$original_base = $amounts['base_amount'];
$original_vat = $amounts['vat_amount'];
$original_total = $amounts['total_amount'];
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
(declaration_id, ca3_line, line_label, base_amount, vat_amount, total_amount, created_date)
VALUES (" . $declaration_id . ", '" . $this->db->escape($ca3_line) . "',
'" . $this->db->escape($line_label) . "', " . $rounded_base . ",
" . $rounded_vat . ", " . $rounded_total . ", NOW())";
$result = $this->db->query($sql);
// Store original amounts in a separate way - we'll use the line_label to store them temporarily
if ($result && ($rounded_vat != $original_vat || $rounded_base != $original_base)) {
$original_info = "ORIGINAL_BASE:" . $original_base . "_ORIGINAL_VAT:" . $original_vat . "_ORIGINAL_TOTAL:" . $original_total;
$update_sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
SET line_label = CONCAT(line_label, '|', '" . $this->db->escape($original_info) . "')
WHERE declaration_id = " . $declaration_id . " AND ca3_line = '" . $this->db->escape($ca3_line) . "'";
$this->db->query($update_sql);
}
return $result !== false;
}
/**
* Update declaration totals
*
* @param int $declaration_id Declaration ID
* @param float $total_vat_collected Total VAT collected
* @param float $total_vat_deductible Total VAT deductible
* @param float $net_vat_due Net VAT due
* @param float $vat_credit VAT credit
* @return bool Success
*/
public function updateDeclarationTotals($declaration_id, $total_vat_collected, $total_vat_deductible, $net_vat_due, $vat_credit)
{
// Round totals to whole numbers
$rounded_collected = round($total_vat_collected);
$rounded_deductible = round($total_vat_deductible);
$rounded_net_due = round($net_vat_due);
$rounded_credit = round($vat_credit);
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET total_vat_collected = " . $rounded_collected . ",
total_vat_deductible = " . $rounded_deductible . ",
net_vat_due = " . $rounded_net_due . ",
vat_credit = " . $rounded_credit . "
WHERE rowid = " . $declaration_id;
$result = $this->db->query($sql);
return $result !== false;
}
/**
* Get period information
*
* @param int $period_id Period ID
* @return array|false Period information or false if not found
*/
public function getPeriod($period_id)
{
$sql = "SELECT * FROM " . MAIN_DB_PREFIX . "declarationtva_periods
WHERE rowid = " . $period_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
return $this->db->fetch_array($result);
}
return false;
}
/**
* Generate declaration number
*
* @param array $period Period information
* @return string Declaration number
*/
public function generateDeclarationNumber($period)
{
$year = date('Y', strtotime($period['start_date']));
$start_month = date('m', strtotime($period['start_date']));
$end_month = date('m', strtotime($period['end_date']));
if ($start_month == $end_month) {
$month_part = $start_month;
} else {
$month_part = $start_month . '-' . $end_month;
}
return 'CA3-' . $year . '-' . $month_part . '-' . date('YmdHis');
}
/**
* Generate declaration number from dates
*
* @param string $start_date Start date
* @param string $end_date End date
* @return string Declaration number
*/
public function generateDeclarationNumberFromDates($start_date, $end_date)
{
$year = date('Y', strtotime($start_date));
$start_month = date('m', strtotime($start_date));
$end_month = date('m', strtotime($end_date));
if ($start_month == $end_month) {
$month_part = $start_month;
} else {
$month_part = $start_month . '-' . $end_month;
}
return 'CA3-' . $year . '-' . $month_part . '-' . date('YmdHis');
}
/**
* Get declaration information
*
* @param int $declaration_id Declaration ID
* @return array|false Declaration information or false if not found
*/
public function getDeclaration($declaration_id)
{
$sql = "SELECT d.*, p.period_name, p.start_date, p.end_date
FROM " . MAIN_DB_PREFIX . "declarationtva_declarations d
LEFT JOIN " . MAIN_DB_PREFIX . "declarationtva_periods p ON d.period_id = p.rowid
WHERE d.rowid = " . $declaration_id . " AND d.entity = " . $this->entity;
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
return $this->db->fetch_array($result);
}
return false;
}
/**
* Get CA-3 lines for a declaration
*
* @param int $declaration_id Declaration ID
* @return array CA-3 lines
*/
public function getCA3Lines($declaration_id)
{
$sql = "SELECT * FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . $declaration_id . "
ORDER BY ca3_line";
$result = $this->db->query($sql);
$lines = array();
if ($result) {
while ($obj = $this->db->fetch_object($result)) {
$lines[] = array(
'ca3_line' => $obj->ca3_line,
'line_label' => $obj->line_label,
'base_amount' => $obj->base_amount,
'vat_amount' => $obj->vat_amount,
'total_amount' => $obj->total_amount
);
}
}
return $lines;
}
/**
* Delete a declaration
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function deleteDeclaration($declaration_id)
{
$this->db->begin();
// Check if declaration exists and is in draft status
$sql = "SELECT status FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if ($result) {
$obj = $this->db->fetch_object($result);
if ($obj && $obj->status == 'draft') {
// Delete CA-3 lines first
$sql_lines = "DELETE FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . (int)$declaration_id;
$this->db->query($sql_lines);
// Delete declaration
$sql_declaration = "DELETE FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql_declaration);
if ($result) {
$this->db->commit();
return true;
}
}
}
$this->db->rollback();
return false;
}
/**
* Recalculate CA-3 amounts for a declaration
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function recalculateCA3Amounts($declaration_id)
{
// Get declaration info directly from database
$sql = "SELECT start_date, end_date FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . $declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if (!$result || $this->db->num_rows($result) == 0) {
return false;
}
$obj = $this->db->fetch_object($result);
// Clear existing CA-3 lines
$sql = "DELETE FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . $declaration_id;
$this->db->query($sql);
// Recalculate using declaration dates
$period = array(
'start_date' => $obj->start_date,
'end_date' => $obj->end_date
);
return $this->calculateCA3Amounts($declaration_id, $period);
}
/**
* Get VAT credit carry-forward from previous periods
*/
public function getVATCreditCarryForward($declaration_start_date)
{
// Get journal configuration to use the correct account
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$journal_config = $config->getJournalConfiguration();
$vat_to_receive_account = $journal_config['vat_to_receive'];
// Check VAT credit account balance at declaration start date
$sql = "SELECT SUM(debit - credit) as balance
FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping
WHERE account_number = '" . $this->db->escape($vat_to_receive_account) . "'
AND doc_date < '" . $declaration_start_date . "'";
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
$obj = $this->db->fetch_object($result);
return max(0, $obj->balance); // Only positive balances (credits)
}
return 0;
}
/**
* Create accounting entries for declaration submission
*
* @param int $declaration_id Declaration ID
* @param object $user User object
* @return bool Success
*/
public function createAccountingEntries($declaration_id, $user)
{
// Get declaration data
$fetch_result = $this->fetch($declaration_id);
if (!$fetch_result) {
$this->error = 'Declaration not found';
return false;
}
// Create a declaration object with the fetched data
$declaration = new stdClass();
$declaration->rowid = $this->rowid;
$declaration->declaration_name = $this->declaration_name;
$declaration->start_date = $this->start_date;
$declaration->end_date = $this->end_date;
// Get CA-3 lines data
$ca3_lines = $this->getCA3Lines($declaration_id);
if (empty($ca3_lines)) {
$this->error = 'No CA-3 lines found for declaration';
return false;
}
// Debug: Log CA-3 lines found
error_log("DEBUG: Found " . count($ca3_lines) . " CA-3 lines for declaration " . $declaration_id);
// Create lookup array for CA-3 data
$ca3_lookup = array();
foreach ($ca3_lines as $line) {
$ca3_lookup[$line['ca3_line']] = $line;
}
// Get journal configuration
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$journal_config = $config->getJournalConfiguration();
// Create OD journal entries
$od_entries = $this->createODJournalEntries($declaration, $ca3_lookup, $journal_config, $user);
error_log("DEBUG: OD entries result: " . ($od_entries ? "SUCCESS" : "FAILED"));
if (!$od_entries) {
return false;
}
// Create bank journal entries
$bank_entries = $this->createBankJournalEntries($declaration, $ca3_lookup, $journal_config, $user);
if (!$bank_entries) {
return false;
}
return true;
}
/**
* Create OD journal entries for VAT accounts
*
* @param object $declaration Declaration object
* @param array $ca3_lookup CA-3 data lookup array
* @param array $journal_config Journal configuration
* @param object $user User object
* @return bool Success
*/
private function createODJournalEntries($declaration, $ca3_lookup, $journal_config, $user)
{
$entries = array();
// Get line 8 VAT accounts (debit side)
$line8_entries = $this->getLine8VATAccountsForAccounting($declaration, $ca3_lookup);
error_log("DEBUG: Line 8 entries: " . count($line8_entries));
$entries = array_merge($entries, $line8_entries);
// Get line 20 accounts (credit side)
$line20_entries = $this->getLine20AccountsForAccounting($declaration, $ca3_lookup);
error_log("DEBUG: Line 20 entries: " . count($line20_entries));
$entries = array_merge($entries, $line20_entries);
// Add balancing entry
$balancing_entry = $this->getVATResultEntryForAccounting($declaration, $ca3_lookup, $journal_config);
error_log("DEBUG: Balancing entry: " . ($balancing_entry ? "FOUND" : "NONE"));
if ($balancing_entry) {
$entries[] = $balancing_entry;
}
error_log("DEBUG: Total OD entries to create: " . count($entries));
// Create accounting entries in Dolibarr
return $this->saveAccountingEntries($entries, $declaration, $user);
}
/**
* Create bank journal entries for payments/refunds
*
* @param object $declaration Declaration object
* @param array $ca3_lookup CA-3 data lookup array
* @param array $journal_config Journal configuration
* @param object $user User object
* @return bool Success
*/
private function createBankJournalEntries($declaration, $ca3_lookup, $journal_config, $user)
{
// Get bank account configuration
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();
$bank_account_id = $bank_config['bank_account'];
error_log("DEBUG: Bank account ID from config: " . $bank_account_id);
if (empty($bank_account_id) || $bank_account_id == 0) {
// No bank account configured - skip bank entries
error_log("DEBUG: No bank account configured - skipping bank entries");
return true;
}
// Get bank account details
$bank_account = $this->getBankAccountDetails($bank_account_id);
if (!$bank_account) {
$this->error = 'Bank account not found';
return false;
}
// Calculate VAT amounts
$line_16_amount = $this->getLineAmount($declaration->rowid, '16');
$line_23_amount = $this->getLineAmount($declaration->rowid, '23');
$td_amount = $line_16_amount - $line_23_amount;
$entries = array();
if ($td_amount > 0) {
// VAT payment case - money going out
$entries[] = array(
'account_code' => $bank_account['account_code'],
'account_label' => $bank_account['account_label'],
'entry_label' => 'Paiement TVA - ' . $declaration->declaration_name,
'debit' => '',
'credit' => $td_amount
);
$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' => $td_amount,
'credit' => ''
);
} elseif ($td_amount < 0) {
// VAT refund case - money coming in
$vat_credit = abs($td_amount);
// Determine which credit account to use based on threshold
$threshold_config = $config->getVATRefundThresholdConfiguration();
$refund_threshold = $threshold_config['refund_threshold'];
$threshold_enabled = $threshold_config['refund_threshold_enabled'];
if ($threshold_enabled && $vat_credit < $refund_threshold) {
// Use carry-forward account
$vat_account = $journal_config['vat_to_receive'];
} else {
// Use immediate refund account
$vat_account = $journal_config['vat_refund'];
}
$entries[] = array(
'account_code' => $bank_account['account_code'],
'account_label' => $bank_account['account_label'],
'entry_label' => 'Remboursement TVA - ' . $declaration->declaration_name,
'debit' => $vat_credit,
'credit' => ''
);
$entries[] = array(
'account_code' => $vat_account,
'account_label' => $this->getAccountLabel($vat_account),
'entry_label' => 'Remboursement TVA - ' . $declaration->declaration_name,
'debit' => '',
'credit' => $vat_credit
);
}
// Create accounting entries in Dolibarr
return $this->saveAccountingEntries($entries, $declaration, $user);
}
/**
* Get line 8 VAT accounts for accounting entries
*
* @param object $declaration Declaration object
* @param array $ca3_lookup CA-3 data lookup array
* @return array Accounting entries
*/
private function getLine8VATAccountsForAccounting($declaration, $ca3_lookup)
{
$entries = array();
if (!isset($ca3_lookup['08'])) {
return $entries;
}
error_log("DEBUG: Declaration object rowid: " . ($declaration->rowid ?? 'NULL'));
$line8_details = $this->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' => $account['vat_amount'],
'credit' => 0
);
}
}
return $entries;
}
/**
* Get line 20 accounts for accounting entries
*
* @param object $declaration Declaration object
* @param array $ca3_lookup CA-3 data lookup array
* @return array Accounting entries
*/
private function getLine20AccountsForAccounting($declaration, $ca3_lookup)
{
$entries = array();
if (!isset($ca3_lookup['20'])) {
return $entries;
}
$line20_details = $this->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' => 0,
'credit' => $account['vat_amount']
);
}
}
return $entries;
}
/**
* Get VAT result entry for accounting
*
* @param object $declaration Declaration object
* @param array $ca3_lookup CA-3 data lookup array
* @param array $journal_config Journal configuration
* @return array|null Accounting entry
*/
private function getVATResultEntryForAccounting($declaration, $ca3_lookup, $journal_config)
{
// Get TD line amount (Line 16 - Line 23)
$line_16_amount = $this->getLineAmount($declaration->rowid, '16');
$line_23_amount = $this->getLineAmount($declaration->rowid, '23');
$td_amount = $line_16_amount - $line_23_amount;
if ($td_amount > 0) {
// VAT due case
return array(
'account_code' => $journal_config['vat_to_pay'],
'account_label' => $this->getAccountLabel($journal_config['vat_to_pay']),
'entry_label' => $declaration->declaration_name,
'debit' => 0,
'credit' => $td_amount
);
} elseif ($td_amount < 0) {
// VAT credit case
$vat_credit = abs($td_amount);
// Determine which credit account to use based on threshold
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$threshold_config = $config->getVATRefundThresholdConfiguration();
$refund_threshold = $threshold_config['refund_threshold'];
$threshold_enabled = $threshold_config['refund_threshold_enabled'];
if ($threshold_enabled && $vat_credit < $refund_threshold) {
// Use carry-forward account
$vat_account = $journal_config['vat_to_receive'];
} else {
// Use immediate refund account
$vat_account = $journal_config['vat_refund'];
}
return array(
'account_code' => $vat_account,
'account_label' => $this->getAccountLabel($vat_account),
'entry_label' => $declaration->declaration_name,
'debit' => $vat_credit,
'credit' => 0
);
}
return null;
}
/**
* Save accounting entries to Dolibarr
*
* @param array $entries Accounting entries
* @param object $declaration Declaration object
* @param object $user User object
* @return bool Success
*/
private function saveAccountingEntries($entries, $declaration, $user)
{
error_log("DEBUG: saveAccountingEntries called with user: " . ($user ? "VALID" : "NULL"));
if (empty($entries)) {
return true; // No entries to save
}
// Use Dolibarr's bookkeeping class with correct field mapping
require_once DOL_DOCUMENT_ROOT . '/accountancy/class/bookkeeping.class.php';
foreach ($entries as $entry) {
error_log("DEBUG: Creating accounting entry for account: " . $entry['account_code'] . ", debit: " . $entry['debit'] . ", credit: " . $entry['credit']);
// Prepare amounts
$debit_amount = !empty($entry['debit']) ? (float)$entry['debit'] : 0;
$credit_amount = !empty($entry['credit']) ? (float)$entry['credit'] : 0;
// Create debit entry if amount > 0
if ($debit_amount > 0) {
$debit = new Bookkeeping($this->db);
$debit->doc_date = $declaration->end_date;
$debit->doc_ref = $declaration->declaration_name;
$debit->code_journal = 'OD';
$debit->numero_compte = $entry['account_code'];
$debit->label_compte = $entry['account_label'];
$debit->montant = $debit_amount;
$debit->sens = 'D';
$debit->fk_doc = $declaration->rowid;
$debit->fk_docdet = 0;
$debit->societe_type = 1;
$debit->fk_user_author = $user->id;
$debit->code_tiers = '';
$debit->piece_num = '';
$debit->import_key = '';
$debit->entity = $this->entity;
$result = $debit->create($user);
error_log("DEBUG: Debit entry create result: " . ($result ? "SUCCESS" : "FAILED"));
if ($result > 0) {
error_log("DEBUG: Debit entry created successfully with ID: " . $result);
} else {
error_log("DEBUG: Debit entry creation failed: " . $debit->error);
$this->error = 'Failed to create debit entry: ' . $debit->error;
return false;
}
}
// Create credit entry if amount > 0
if ($credit_amount > 0) {
$credit = new Bookkeeping($this->db);
$credit->doc_date = $declaration->end_date;
$credit->doc_ref = $declaration->declaration_name;
$credit->code_journal = 'OD';
$credit->numero_compte = $entry['account_code'];
$credit->label_compte = $entry['account_label'];
$credit->montant = $credit_amount;
$credit->sens = 'C';
$credit->fk_doc = $declaration->rowid;
$credit->fk_docdet = 0;
$credit->societe_type = 1;
$credit->fk_user_author = $user->id;
$credit->code_tiers = '';
$credit->piece_num = '';
$credit->import_key = '';
$credit->entity = $this->entity;
$result = $credit->create($user);
error_log("DEBUG: Credit entry create result: " . ($result ? "SUCCESS" : "FAILED"));
if ($result > 0) {
error_log("DEBUG: Credit entry created successfully with ID: " . $result);
} else {
error_log("DEBUG: Credit entry creation failed: " . $credit->error);
$this->error = 'Failed to create credit entry: ' . $credit->error;
return false;
}
}
}
// Final verification - check all entries created for this declaration
$final_check_sql = "SELECT COUNT(*) as total_entries FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping
WHERE doc_ref = '" . $this->db->escape($declaration->declaration_name) . "'";
$final_check_result = $this->db->query($final_check_sql);
if ($final_check_result && $this->db->num_rows($final_check_result) > 0) {
$final_check_obj = $this->db->fetch_object($final_check_result);
error_log("DEBUG: Final verification - Total entries in database for declaration " . $declaration->declaration_name . ": " . $final_check_obj->total_entries);
}
// Also check what's actually in the accounting_bookkeeping table
$debug_sql = "SELECT doc_ref, numero_compte, montant, sens FROM " . MAIN_DB_PREFIX . "accounting_bookkeeping
WHERE doc_ref LIKE '%AOUT%' ORDER BY rowid DESC LIMIT 10";
$debug_result = $this->db->query($debug_sql);
if ($debug_result && $this->db->num_rows($debug_result) > 0) {
error_log("DEBUG: Recent entries in accounting_bookkeeping table:");
while ($debug_obj = $this->db->fetch_object($debug_result)) {
error_log("DEBUG: Entry - doc_ref: " . $debug_obj->doc_ref . ", account: " . $debug_obj->numero_compte . ", amount: " . $debug_obj->montant . ", sens: " . $debug_obj->sens);
}
}
return true;
}
/**
* Get bank account details
*
* @param int $bank_account_id Bank account ID
* @return array|false Bank account details
*/
private function getBankAccountDetails($bank_account_id)
{
$sql = "SELECT ba.rowid, ba.label, ba.number, a.account_number as account_code, a.label as account_label
FROM " . MAIN_DB_PREFIX . "bank_account ba
LEFT JOIN " . MAIN_DB_PREFIX . "accounting_account a ON a.rowid = ba.account_number
WHERE ba.rowid = " . (int)$bank_account_id . " AND ba.entity = " . $this->entity;
error_log("DEBUG: Bank account query: " . $sql);
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
$bank_data = $this->db->fetch_array($result);
error_log("DEBUG: Bank account data: " . print_r($bank_data, true));
return $bank_data;
}
error_log("DEBUG: No bank account found for ID: " . $bank_account_id);
return false;
}
/**
* Get account label from account code
*
* @param string $account_code Account code
* @return string Account label
*/
private function getAccountLabel($account_code)
{
$sql = "SELECT label FROM " . MAIN_DB_PREFIX . "accounting_account
WHERE account_number = '" . $this->db->escape($account_code) . "'
AND entity = " . $this->entity;
$result = $this->db->query($sql);
if ($result && $this->db->num_rows($result) > 0) {
$obj = $this->db->fetch_object($result);
return $obj->label;
}
return $account_code; // Fallback to account code
}
/**
* Submit declaration (create accounting entries and update status)
*
* @param int $declaration_id Declaration ID
* @param object $user User object
* @return bool Success
*/
public function submitDeclaration($declaration_id, $user)
{
error_log("DEBUG: submitDeclaration called with user: " . ($user ? "VALID" : "NULL"));
// Get declaration data
$declaration = $this->fetch($declaration_id);
if (!$declaration) {
$this->error = 'Declaration not found';
return false;
}
// Check if declaration is validated
// TEMPORARILY DISABLED FOR TESTING - allows re-submission
/*
if ($declaration->status !== 'validated') {
$this->error = 'Declaration must be validated before submission';
return false;
}
*/
// Check if accounting entries are enabled
require_once DOL_DOCUMENT_ROOT . '/custom/declarationtva/core/class/declarationtva_config.class.php';
$config = new DeclarationTVA_Config($this->db, $this->entity);
$auto_create_config = $config->getAutoCreateAccountingConfiguration();
if (!isset($auto_create_config['auto_create_accounting']) || !$auto_create_config['auto_create_accounting']) {
// Accounting entries disabled - just update status
return $this->updateDeclarationStatus($declaration_id, 'submitted');
}
// Create accounting entries
error_log("DEBUG: About to call createAccountingEntries with user: " . ($user ? "VALID" : "NULL"));
$accounting_result = $this->createAccountingEntries($declaration_id, $user);
if (!$accounting_result) {
$this->error = 'Failed to create accounting entries: ' . $this->error;
return false;
}
// Update declaration status to submitted
$status_result = $this->updateDeclarationStatus($declaration_id, 'submitted');
if (!$status_result) {
$this->error = 'Failed to update declaration status: ' . $this->error;
return false;
}
return true;
}
/**
* Reset declaration status to validated (for testing purposes)
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function resetDeclarationStatus($declaration_id)
{
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'validated'
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if (!$result) {
$this->error = 'Database error: ' . $this->db->lasterror();
return false;
}
return true;
}
/**
* Update declaration status
*
* @param int $declaration_id Declaration ID
* @param string $status New status
* @return bool Success
*/
private function updateDeclarationStatus($declaration_id, $status)
{
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = '" . $this->db->escape($status) . "'
WHERE rowid = " . (int)$declaration_id;
$result = $this->db->query($sql);
if (!$result) {
$this->error = 'Database error: ' . $this->db->lasterror();
return false;
}
return true;
}
/**
* Validate accounting entries balance
*
* @param array $entries Accounting entries
* @return bool Is balanced
*/
private function validateAccountingBalance($entries)
{
$total_debit = 0;
$total_credit = 0;
foreach ($entries as $entry) {
$total_debit += (float)$entry['debit'];
$total_credit += (float)$entry['credit'];
}
// Allow for small rounding differences (0.01)
return abs($total_debit - $total_credit) < 0.01;
}
/**
* Get account mappings with labels from chart of accounts
*
* @return array Array of account mappings with labels
*/
public function getAccountMappings()
{
$sql = "SELECT m.rowid, m.ca3_line, m.account_code, m.account_label, m.vat_rate, m.is_active,
a.label as account_label_pcg
FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings m
LEFT JOIN " . MAIN_DB_PREFIX . "accounting_account a ON a.account_number = m.account_code AND a.entity = m.entity
WHERE m.entity = " . $this->entity . " AND m.is_active = 1
ORDER BY m.ca3_line, m.account_code";
$result = $this->db->query($sql);
$mappings = array();
if ($result) {
while ($obj = $this->db->fetch_object($result)) {
// Use the label from chart of accounts if available, otherwise use the stored label
$account_label = !empty($obj->account_label_pcg) ? $obj->account_label_pcg : $obj->account_label;
$mappings[] = array(
'rowid' => $obj->rowid,
'ca3_line' => $obj->ca3_line,
'account_code' => $obj->account_code,
'account_label' => $account_label,
'vat_rate' => $obj->vat_rate,
'is_active' => $obj->is_active
);
}
}
return $mappings;
}
/**
* Get detailed account breakdown for a specific CA-3 line
*
* @param int $declaration_id Declaration ID
* @param string $ca3_line CA-3 line code (e.g., 'A1', '08', '09', etc.)
* @return array Detailed breakdown with accounts and amounts
*/
public function getCA3LineDetails($declaration_id, $ca3_line)
{
error_log("DEBUG: getCA3LineDetails called for declaration $declaration_id, line $ca3_line");
// Get declaration dates
$sql = "SELECT start_date, end_date FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if (!$result || $this->db->num_rows($result) == 0) {
error_log("DEBUG: No declaration dates found for declaration $declaration_id");
return array();
}
$obj = $this->db->fetch_object($result);
$start_date = $obj->start_date;
$end_date = $obj->end_date;
// Get account mappings for this CA-3 line
$mappings = $this->getAccountMappings();
$line_mappings = array();
// Find mappings for this specific line
foreach ($mappings as $mapping) {
// Handle special cases for lines 08, 09, 9B which have _BASE and _VAT suffixes
if (in_array($ca3_line, array('08', '09', '9B'))) {
if ($mapping['ca3_line'] == $ca3_line . '_BASE' || $mapping['ca3_line'] == $ca3_line . '_VAT') {
$line_mappings[] = $mapping;
}
} else {
// Normal matching for other lines
if ($mapping['ca3_line'] == $ca3_line) {
$line_mappings[] = $mapping;
}
}
}
$details = array();
$total_base = 0;
$total_vat = 0;
$total_amount = 0;
// Process each mapped account
foreach ($line_mappings as $mapping) {
$account_code = $mapping['account_code'];
$account_label = $mapping['account_label'];
// Get account amounts for this period
$amounts = $this->getAccountAmounts($account_code, $start_date, $end_date);
$account_detail = array(
'account_code' => $account_code,
'account_label' => $account_label,
'base_amount' => $amounts['base_amount'],
'vat_amount' => $amounts['vat_amount'],
'total_amount' => $amounts['total_amount'],
'mapping_type' => $mapping['ca3_line'] // Will be 'A1', '08_BASE', '08_VAT', etc.
);
$details[] = $account_detail;
// Accumulate totals
$total_base += $amounts['base_amount'];
$total_vat += $amounts['vat_amount'];
$total_amount += $amounts['total_amount'];
}
// Get the calculated line total from CA-3 lines table
$sql_line = "SELECT base_amount, vat_amount, total_amount, line_label
FROM " . MAIN_DB_PREFIX . "declarationtva_ca3_lines
WHERE declaration_id = " . (int)$declaration_id . "
AND ca3_line = '" . $this->db->escape($ca3_line) . "'";
$result_line = $this->db->query($sql_line);
$calculated_line = null;
if ($result_line && $this->db->num_rows($result_line) > 0) {
$calculated_line = $this->db->fetch_object($result_line);
}
$result = array(
'ca3_line' => $ca3_line,
'declaration_id' => $declaration_id,
'start_date' => $start_date,
'end_date' => $end_date,
'account_details' => $details,
'calculated_line' => $calculated_line,
'totals' => array(
'base_amount' => $total_base,
'vat_amount' => $total_vat,
'total_amount' => $total_amount
),
'account_count' => count($details)
);
error_log("DEBUG: getCA3LineDetails returning " . count($details) . " account details for line $ca3_line");
return $result;
}
/**
* Validate a declaration
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function validateDeclaration($declaration_id)
{
// First, check if the validated_date and validated_by columns exist
$sql_check_columns = "SHOW COLUMNS FROM " . MAIN_DB_PREFIX . "declarationtva_declarations LIKE 'validated_date'";
$result_check = $this->db->query($sql_check_columns);
$has_validated_columns = ($result_check && $this->db->num_rows($result_check) > 0);
if ($has_validated_columns) {
// Use the enhanced version with validated_date and validated_by
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'validated',
validated_date = NOW(),
validated_by = " . $this->db->escape($GLOBALS['user']->id) . "
WHERE rowid = " . (int)$declaration_id . "
AND entity = " . $this->entity;
} else {
// Use the basic version without the additional columns
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'validated'
WHERE rowid = " . (int)$declaration_id . "
AND entity = " . $this->entity;
}
$result = $this->db->query($sql);
if (!$result) {
$this->error = "Database error updating declaration: " . $this->db->lasterror();
return false;
}
// Check if the update actually affected any rows
if ($this->db->affected_rows($result) == 0) {
$this->error = "No declaration found with ID " . $declaration_id . " or insufficient permissions";
return false;
}
return true;
}
/**
* Unvalidate a declaration (for testing purposes)
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function unvalidateDeclaration($declaration_id)
{
// Check if the validated_date and validated_by columns exist
$sql_check_columns = "SHOW COLUMNS FROM " . MAIN_DB_PREFIX . "declarationtva_declarations LIKE 'validated_date'";
$result_check = $this->db->query($sql_check_columns);
$has_validated_columns = ($result_check && $this->db->num_rows($result_check) > 0);
if ($has_validated_columns) {
// Use the enhanced version with validated_date and validated_by
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'draft',
validated_date = NULL,
validated_by = NULL
WHERE rowid = " . (int)$declaration_id . "
AND entity = " . $this->entity;
} else {
// Use the basic version without the additional columns
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'draft'
WHERE rowid = " . (int)$declaration_id . "
AND entity = " . $this->entity;
}
$result = $this->db->query($sql);
if (!$result) {
$this->error = "Database error updating declaration: " . $this->db->lasterror();
return false;
}
// Check if the update actually affected any rows
if ($this->db->affected_rows($result) == 0) {
$this->error = "No declaration found with ID " . $declaration_id . " or insufficient permissions";
return false;
}
return true;
}
/**
* Save validated PDF to Dolibarr documents
*
* @param int $declaration_id Declaration ID
* @param string $pdf_path Path to the PDF file
* @return bool Success
*/
public function saveValidatedPDF($declaration_id, $pdf_path)
{
global $conf, $user;
try {
// Create VAT declarations validated directory structure
$vat_declarations_dir = DOL_DATA_ROOT . '/declarationtva/validated/';
// Create directory if it doesn't exist
if (!is_dir($vat_declarations_dir)) {
if (!dol_mkdir($vat_declarations_dir)) {
$this->error = "Failed to create VAT declarations directory: " . $vat_declarations_dir;
return false;
}
}
// Check if PDF is already in the correct location
if (strpos($pdf_path, $vat_declarations_dir) === 0) {
// PDF is already in the correct location, no need to copy
$dest_path = $pdf_path;
} else {
// Generate filename with proper structure
$filename = 'CA3_' . $this->declaration_number . '_' . date('Y-m-d') . '.pdf';
$dest_path = $vat_declarations_dir . $filename;
// Copy PDF to validated directory
if (!copy($pdf_path, $dest_path)) {
$this->error = "Failed to copy PDF to validated directory";
return false;
}
}
// Try to add document record to Dolibarr (optional feature)
// Skip ECM integration if there are any issues to prevent fatal errors
$ecm_integration_enabled = false; // Temporarily disabled to prevent fatal errors
if ($ecm_integration_enabled) {
// Try multiple possible paths for ecmfiles.class.php
$ecmfiles_paths = array(
DOL_DOCUMENT_ROOT . '/ecm/class/ecmfiles.class.php',
dirname(__FILE__) . '/../../../ecm/class/ecmfiles.class.php',
dirname(__FILE__) . '/../../../../ecm/class/ecmfiles.class.php',
DOL_DOCUMENT_ROOT . '/core/class/ecmfiles.class.php',
dirname(__FILE__) . '/../../../../core/class/ecmfiles.class.php'
);
$ecmfiles_path = null;
foreach ($ecmfiles_paths as $path) {
if (file_exists($path)) {
$ecmfiles_path = $path;
break;
}
}
if ($ecmfiles_path && file_exists($ecmfiles_path)) {
try {
// Log the path being used for debugging
require_once $ecmfiles_path;
$ecmfile = new EcmFiles($this->db);
$ecmfile->filepath = 'declarationtva/' . date('Y') . '/' . date('m') . '/';
$ecmfile->filename = $filename;
$ecmfile->label = 'CA-3 Déclaration TVA - ' . $this->declaration_number;
$ecmfile->description = 'Déclaration TVA CA-3 validée avec détail des comptes pour la période ' . date('Y-m');
$ecmfile->entity = $this->entity;
$ecmfile->fk_user_author = $user->id;
$ecmfile->fk_user_modif = $user->id;
$ecmfile->datec = dol_now();
$ecmfile->tms = dol_now();
$ecmfile->mimetype = 'application/pdf';
$ecmfile->filesize = filesize($dest_path);
$ecmfile->checksum = md5_file($dest_path);
$result = $ecmfile->create($user);
if ($result > 0) {
// Try to link document to declaration (this might fail if table doesn't exist)
$link_result = $this->linkDocumentToDeclaration($declaration_id, $ecmfile->id);
if (!$link_result) {
// Log the error but don't fail the whole process
}
} else {
// Log the error but don't fail the whole process
}
} catch (Exception $e) {
// Log the error but don't fail the whole process
}
} else {
// Log that ECM files class is not available
}
}
// Return true even if ECM file creation failed - the PDF is still saved to disk
return true;
} catch (Exception $e) {
$this->error = "Exception in saveValidatedPDF: " . $e->getMessage();
return false;
}
}
/**
* Link document to declaration
*
* @param int $declaration_id Declaration ID
* @param int $document_id Document ID
* @return bool Success
*/
private function linkDocumentToDeclaration($declaration_id, $document_id)
{
// First check if the table exists
$sql_check = "SHOW TABLES LIKE '" . MAIN_DB_PREFIX . "declarationtva_documents'";
$result_check = $this->db->query($sql_check);
if (!$result_check || $this->db->num_rows($result_check) == 0) {
$this->error = "Documents table does not exist. Please run the database migration.";
return false;
}
$sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_documents
(declaration_id, document_id, created_date, entity)
VALUES (" . (int)$declaration_id . ", " . (int)$document_id . ", NOW(), " . $this->entity . ")";
$result = $this->db->query($sql);
if (!$result) {
$this->error = "Database error linking document: " . $this->db->lasterror();
return false;
}
return true;
}
/**
* Check if declaration has a validated document
*
* @param int $declaration_id Declaration ID
* @return bool Has document
*/
public function hasValidatedDocument($declaration_id)
{
// Get declaration details
$sql = "SELECT declaration_number, status FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if (!$result || $this->db->num_rows($result) == 0) {
return false;
}
$obj = $this->db->fetch_object($result);
// Only check for documents if declaration is validated
if ($obj->status != 'validated') {
return false;
}
// Check if PDF file exists in the VAT declarations validated directory
$vat_declarations_dir = DOL_DATA_ROOT . '/declarationtva/validated/';
// Look for PDF files with the declaration ID (not declaration number)
$pattern = $vat_declarations_dir . 'CA3_' . $declaration_id . '_*.pdf';
$files = glob($pattern);
// If no files found with the exact pattern, try a more flexible search
if (empty($files)) {
$pattern = $vat_declarations_dir . 'CA3_*' . $declaration_id . '*.pdf';
$files = glob($pattern);
}
return !empty($files) && file_exists($files[0]);
}
/**
* Get the PDF file path for a validated declaration
*
* @param int $declaration_id Declaration ID
* @return string|false PDF file path or false if not found
*/
public function getValidatedPDFPath($declaration_id)
{
// Get declaration details
$sql = "SELECT declaration_number, status FROM " . MAIN_DB_PREFIX . "declarationtva_declarations
WHERE rowid = " . (int)$declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
if (!$result || $this->db->num_rows($result) == 0) {
return false;
}
$obj = $this->db->fetch_object($result);
// Only return path if declaration is validated
if ($obj->status != 'validated') {
return false;
}
// Check if PDF file exists in the VAT declarations validated directory
$vat_declarations_dir = DOL_DATA_ROOT . '/declarationtva/validated/';
// Look for PDF files with the declaration ID (not declaration number)
$pattern = $vat_declarations_dir . 'CA3_' . $declaration_id . '_*.pdf';
$files = glob($pattern);
// If no files found with the exact pattern, try a more flexible search
if (empty($files)) {
$pattern = $vat_declarations_dir . 'CA3_*' . $declaration_id . '*.pdf';
$files = glob($pattern);
}
if (!empty($files) && file_exists($files[0])) {
return $files[0];
}
return false;
}
/**
* Get the VAT declarations documents directory path
*
* @param string $year Year (optional, defaults to current year)
* @param string $month Month (optional, defaults to current month)
* @return string Directory path
*/
public function getVATDeclarationsDirectory($year = null, $month = null)
{
if ($year === null) {
$year = date('Y');
}
if ($month === null) {
$month = date('m');
}
return DOL_DATA_ROOT . '/documents/declarationtva/' . $year . '/' . $month . '/';
}
/**
* Get the relative path for ECM files
*
* @param string $year Year (optional, defaults to current year)
* @param string $month Month (optional, defaults to current month)
* @return string Relative path
*/
public function getVATDeclarationsRelativePath($year = null, $month = null)
{
if ($year === null) {
$year = date('Y');
}
if ($month === null) {
$month = date('m');
}
return 'declarationtva/' . $year . '/' . $month . '/';
}
}