DeclarationTVA/core/class/declarationtva.class.php
Frank Cools 1cf0d3d6a5 Fix D-section calculation logic
Fixed:
- Only sections B and C VAT amounts are summed for totals
- Section A lines (A1-A5) are excluded from totals calculation
- Only VAT amounts (not base amounts) are included in totals
- This should fix line 25 showing correct value (406.28)
2025-10-02 22:23:27 +02:00

715 lines
26 KiB
PHP

<?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', '08', '09', '9B', '17', '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])) {
error_log("DeclarationTVA: Processing CA-3 line $ca3_line with " . count($grouped_mappings[$ca3_line]) . " mapped accounts");
foreach ($grouped_mappings[$ca3_line] as $mapping) {
error_log("DeclarationTVA: Processing account " . $mapping['account_code'] . " for line $ca3_line");
$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'];
error_log("DeclarationTVA: Account " . $mapping['account_code'] . " amounts: base=" . $amounts['base_amount'] . ", vat=" . $amounts['vat_amount']);
}
} else {
error_log("DeclarationTVA: No mappings found for CA-3 line $ca3_line");
}
}
// 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';
$this->createCA3Line($declaration_id, $ca3_line, $combined_label, $combined_amounts);
// Log the final amounts for debugging
error_log("DeclarationTVA: Final amounts for line $ca3_line: base=$line_total_base, vat=$line_total_vat, total=$line_total_amount");
// Update totals - only VAT amounts from sections B and C
if (in_array($ca3_line, ['08', '09', '9B', '17'])) {
// Section B: VAT amounts only
$total_vat_collected += $line_total_vat;
} elseif (in_array($ca3_line, ['20', '21', '22'])) {
// Section C: VAT amounts only
$total_vat_deductible += $line_total_vat;
}
}
// Debug totals
error_log("DeclarationTVA: Total VAT collected: $total_vat_collected, Total VAT deductible: $total_vat_deductible");
// 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 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)
{
// Line 25: TVA brute due (Total VAT due)
$line_25_amount = $total_vat_collected;
$this->createCA3Line($declaration_id, '25', 'Calculated from sections A and B', array(
'base_amount' => 0,
'vat_amount' => $line_25_amount,
'total_amount' => $line_25_amount
));
// Line 26: TVA déductible totale (Total deductible VAT)
$line_26_amount = $total_vat_deductible;
$this->createCA3Line($declaration_id, '26', 'Calculated from section C', array(
'base_amount' => 0,
'vat_amount' => $line_26_amount,
'total_amount' => $line_26_amount
));
// Calculate net VAT due
$net_vat_due = $total_vat_collected - $total_vat_deductible;
// Line 28: TVA nette due (Net VAT due) - if positive
if ($net_vat_due > 0) {
$this->createCA3Line($declaration_id, '28', 'Calculated: 25 - 26', array(
'base_amount' => 0,
'vat_amount' => $net_vat_due,
'total_amount' => $net_vat_due
));
} else {
$this->createCA3Line($declaration_id, '28', 'Calculated: 25 - 26', array(
'base_amount' => 0,
'vat_amount' => 0,
'total_amount' => 0
));
}
// Line 29: Crédit de TVA (VAT Credit) - if negative
if ($net_vat_due < 0) {
$vat_credit = abs($net_vat_due);
$this->createCA3Line($declaration_id, '29', 'Calculated: 26 - 25', array(
'base_amount' => 0,
'vat_amount' => $vat_credit,
'total_amount' => $vat_credit
));
} else {
$this->createCA3Line($declaration_id, '29', 'Calculated: 26 - 25', 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);
}
// Log successful query for debugging
error_log("DeclarationTVA: Found data with query: " . substr($sql, 0, 100) . "... Debit: " . $obj->total_debit . ", Credit: " . $obj->total_credit . ", Amount: $total_amount");
return array(
'base_amount' => $total_amount,
'vat_amount' => $total_amount,
'total_amount' => $total_amount
);
}
}
// If no data found, log the account code for debugging
error_log("DeclarationTVA: No data found for account $account_code in any accounting table");
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)
{
$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) . "', " . $amounts['base_amount'] . ",
" . $amounts['vat_amount'] . ", " . $amounts['total_amount'] . ", NOW())";
$result = $this->db->query($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)
{
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET total_vat_collected = " . $total_vat_collected . ",
total_vat_deductible = " . $total_vat_deductible . ",
net_vat_due = " . $net_vat_due . ",
vat_credit = " . $vat_credit . "
WHERE rowid = " . $declaration_id;
$result = $this->db->query($sql);
return $result !== false;
}
/**
* Get account mappings
*
* @return array Account mappings
*/
public function getAccountMappings()
{
$sql = "SELECT ca3_line, account_code, account_label, vat_rate
FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings
WHERE entity = " . $this->entity . " AND is_active = 1
ORDER BY ca3_line";
$result = $this->db->query($sql);
$mappings = array();
if ($result) {
while ($obj = $this->db->fetch_object($result)) {
$mappings[] = array(
'ca3_line' => $obj->ca3_line,
'account_code' => $obj->account_code,
'account_label' => $obj->account_label,
'vat_rate' => $obj->vat_rate
);
}
}
return $mappings;
}
/**
* 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;
}
/**
* Validate declaration
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function validateDeclaration($declaration_id)
{
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'validated'
WHERE rowid = " . $declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
return $result !== false;
}
/**
* Submit declaration
*
* @param int $declaration_id Declaration ID
* @return bool Success
*/
public function submitDeclaration($declaration_id)
{
$sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_declarations
SET status = 'submitted', submission_date = NOW()
WHERE rowid = " . $declaration_id . " AND entity = " . $this->entity;
$result = $this->db->query($sql);
return $result !== false;
}
/**
* 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);
}
}